Blob


1 /*
2 * Copyright (c) 2024 Omar Polo <op@openbsd.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
17 #include <sys/time.h>
18 #include <sys/types.h>
19 #include <sys/socket.h>
21 #include <err.h>
22 #include <errno.h>
23 #include <fcntl.h>
24 #include <limits.h>
25 #include <netdb.h>
26 #include <poll.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 #include <unistd.h>
32 #include "got_opentemp.h"
33 #include "got_version.h"
35 #include "bufio.h"
36 #include "utf8d.h"
38 #define USERAGENT "got-notify-http/" GOT_VERSION_STR
40 static int http_timeout = 300; /* 5 minutes in seconds */
42 __dead static void
43 usage(void)
44 {
45 fprintf(stderr, "usage: %s [-c] -h host -p port path\n",
46 getprogname());
47 exit(1);
48 }
50 static int
51 dial(const char *host, const char *port)
52 {
53 struct addrinfo hints, *res, *res0;
54 const char *cause = NULL;
55 int s, error, save_errno;
57 memset(&hints, 0, sizeof(hints));
58 hints.ai_family = AF_UNSPEC;
59 hints.ai_socktype = SOCK_STREAM;
60 error = getaddrinfo(host, port, &hints, &res0);
61 if (error)
62 errx(1, "failed to resolve %s:%s: %s", host, port,
63 gai_strerror(error));
65 s = -1;
66 for (res = res0; res; res = res->ai_next) {
67 s = socket(res->ai_family, res->ai_socktype,
68 res->ai_protocol);
69 if (s == -1) {
70 cause = "socket";
71 continue;
72 }
74 if (connect(s, res->ai_addr, res->ai_addrlen) == -1) {
75 cause = "connect";
76 save_errno = errno;
77 close(s);
78 errno = save_errno;
79 s = -1;
80 continue;
81 }
83 break;
84 }
86 freeaddrinfo(res0);
87 if (s == -1)
88 err(1, "%s", cause);
89 return s;
90 }
92 static void
93 escape(FILE *fp, const uint8_t *s)
94 {
95 uint32_t codepoint, state;
96 const uint8_t *start = s;
98 state = 0;
99 for (; *s; ++s) {
100 switch (decode(&state, &codepoint, *s)) {
101 case UTF8_ACCEPT:
102 switch (codepoint) {
103 case '"':
104 case '\\':
105 fprintf(fp, "\\%c", *s);
106 break;
107 case '\b':
108 fprintf(fp, "\\b");
109 break;
110 case '\f':
111 fprintf(fp, "\\f");
112 break;
113 case '\n':
114 fprintf(fp, "\\n");
115 break;
116 case '\r':
117 fprintf(fp, "\\r");
118 break;
119 case '\t':
120 fprintf(fp, "\\t");
121 break;
122 default:
123 /* other control characters */
124 if (codepoint < ' ' || codepoint == 0x7F) {
125 fprintf(fp, "\\u%04x", codepoint);
126 break;
128 fwrite(start, 1, s - start + 1, fp);
129 break;
131 start = s + 1;
132 break;
134 case UTF8_REJECT:
135 /* bad UTF-8 sequence; try to recover */
136 fputs("\\uFFFD", fp);
137 state = UTF8_ACCEPT;
138 start = s + 1;
139 break;
144 static void
145 json_field(FILE *fp, const char *key, const char *val, int comma)
147 fprintf(fp, "\"%s\":\"", key);
148 escape(fp, val);
149 fprintf(fp, "\"%s", comma ? "," : "");
152 static void
153 json_author(FILE *fp, const char *type, char *address, int comma)
155 char *gt, *lt, *at, *email, *endname;
157 fprintf(fp, "\"%s\":{", type);
159 gt = strchr(address, '<');
160 if (gt != NULL) {
161 /* long format, e.g. "Omar Polo <op@openbsd.org>" */
163 json_field(fp, "full", address, 1);
165 endname = gt;
166 while (endname > address && endname[-1] == ' ')
167 endname--;
169 *endname = '\0';
170 json_field(fp, "name", address, 1);
172 email = gt + 1;
173 lt = strchr(email, '>');
174 if (lt)
175 *lt = '\0';
177 json_field(fp, "mail", email, 1);
179 at = strchr(email, '@');
180 if (at)
181 *at = '\0';
183 json_field(fp, "user", email, 0);
184 } else {
185 /* short format only shows the username */
186 json_field(fp, "user", address, 0);
189 fprintf(fp, "}%s", comma ? "," : "");
192 static int
193 jsonify_branch_rm(FILE *fp, char *line)
195 char *ref, *id;
197 line = strchr(line, ' ');
198 if (line == NULL)
199 errx(1, "invalid branch rm line");
200 line += strspn(line, " ");
202 ref = line;
204 line = strchr(line, ':');
205 if (line == NULL)
206 errx(1, "invalid branch rm line");
207 *line++ = '\0';
208 id = line + strspn(line, " ");
210 fputc('{', fp);
211 json_field(fp, "type", "branch-deleted", 1);
212 json_field(fp, "ref", ref, 1);
213 json_field(fp, "id", id, 0);
214 fputc('}', fp);
216 return 0;
219 static int
220 jsonify_commit_short(FILE *fp, char *line)
222 char *t, *date, *id, *author, *message;
224 t = line;
225 date = t;
226 if ((t = strchr(t, ' ')) == NULL)
227 errx(1, "malformed line");
228 *t++ = '\0';
230 id = t;
231 if ((t = strchr(t, ' ')) == NULL)
232 errx(1, "malformed line");
233 *t++ = '\0';
235 author = t;
236 if ((t = strchr(t, ' ')) == NULL)
237 errx(1, "malformed line");
238 *t++ = '\0';
240 message = t;
242 fprintf(fp, "{\"type\":\"commit\",\"short\":true,");
243 json_field(fp, "id", id, 1);
244 json_author(fp, "committer", author, 1);
245 json_field(fp, "date", date, 1);
246 json_field(fp, "short_message", message, 0);
247 fprintf(fp, "}");
249 return 0;
252 static int
253 jsonify_commit(FILE *fp, char **line, ssize_t *linesize)
255 const char *errstr;
256 char *author = NULL;
257 char *l;
258 ssize_t linelen;
259 int parent = 0;
260 int msglen = 0, msgwrote = 0;
261 int done = 0;
262 enum {
263 P_FROM,
264 P_VIA,
265 P_DATE,
266 P_PARENT,
267 P_MSGLEN,
268 P_MSG,
269 P_DST,
270 P_SUM,
271 } phase = P_FROM;
273 l = *line;
274 if (strncmp(l, "commit ", 7) != 0)
275 errx(1, "%s: unexpected line: %s", __func__, l);
276 l += 7;
277 fprintf(fp, "{\"type\":\"commit\",\"short\":false,");
278 json_field(fp, "id", l, 1);
280 while (!done) {
281 if ((linelen = getline(line, linesize, stdin)) == -1)
282 break;
284 if ((*line)[linelen - 1] == '\n')
285 (*line)[--linelen] = '\0';
287 l = *line;
288 switch (phase) {
289 case P_FROM:
290 if (strncmp(l, "from: ", 6) != 0)
291 errx(1, "unexpected from line");
292 l += 6;
294 author = strdup(l);
295 if (author == NULL)
296 err(1, "strdup");
298 json_author(fp, "author", l, 1);
300 phase = P_VIA;
301 break;
303 case P_VIA:
304 /* optional */
305 if (!strncmp(l, "via: ", 5)) {
306 l += 5;
307 json_author(fp, "committer", l, 1);
308 phase = P_DATE;
309 break;
312 if (author == NULL) /* impossible */
313 err(1, "from not specified");
314 json_author(fp, "committer", author, 1);
315 free(author);
316 author = NULL;
318 phase = P_DATE;
319 /* fallthrough */
321 case P_DATE:
322 /* optional */
323 if (!strncmp(l, "date: ", 6)) {
324 l += 6;
325 json_field(fp, "date", l, 1);
326 phase = P_PARENT;
327 break;
329 phase = P_PARENT;
330 /* fallthough */
332 case P_PARENT:
333 /* optional - more than one */
334 if (!strncmp(l, "parent ", 7)) {
335 l += 7;
336 l += strcspn(l, ":");
337 l += strspn(l, " ");
339 if (parent == 0) {
340 parent = 1;
341 fprintf(fp, "\"parents\":[");
344 fputc('"', fp);
345 escape(fp, l);
346 fputc('"', fp);
348 break;
350 if (parent != 0) {
351 fprintf(fp, "],");
352 parent = 0;
354 phase = P_MSGLEN;
355 /* fallthrough */
357 case P_MSGLEN:
358 if (strncmp(l, "messagelen: ", 12) != 0)
359 errx(1, "unexpected messagelen line");
360 l += 12;
361 msglen = strtonum(l, 1, INT_MAX, &errstr);
362 if (errstr)
363 errx(1, "message len is %s: %s", errstr, l);
365 phase = P_MSG;
366 break;
368 case P_MSG:
369 /*
370 * The commit message is indented with one extra
371 * space which is not accounted for in messagelen,
372 * but we also strip the trailing \n so that
373 * accounts for it.
375 * Since we read line-by-line and there is always
376 * a \n added at the end of the message,
377 * tolerate one byte less than advertised.
378 */
379 if (*l == ' ') {
380 l++; /* skip leading space */
381 linelen--;
383 if (msgwrote == 0 && linelen != 0) {
384 json_field(fp, "short_message", l, 1);
385 fprintf(fp, "\"message\":\"");
386 escape(fp, l);
387 escape(fp, "\n");
388 msgwrote += linelen;
389 } else if (msgwrote != 0) {
390 escape(fp, l);
391 escape(fp, "\n");
394 msglen -= linelen + 1;
395 if (msglen <= 1) {
396 fprintf(fp, "\",");
397 msgwrote = 0;
398 phase = P_DST;
400 break;
402 case P_DST:
403 /* XXX: ignore the diffstat for now */
404 if (*l == '\0') {
405 fprintf(fp, "\"diffstat\":{},");
406 phase = P_SUM;
407 break;
409 break;
411 case P_SUM:
412 /* XXX: ignore the sum of changes for now */
413 fprintf(fp, "\"changes\":{}}");
414 done = 1;
415 break;
417 default:
418 /* unreachable */
419 errx(1, "unexpected line: %s", *line);
422 if (ferror(stdin))
423 err(1, "getline");
424 if (!done)
425 errx(1, "unexpected EOF");
427 return 0;
430 static int
431 jsonify_tag(FILE *fp, char **line, ssize_t *linesize)
433 const char *errstr;
434 char *l;
435 ssize_t linelen;
436 int msglen = 0, msgwrote = 0;
437 int done = 0;
438 enum {
439 P_FROM,
440 P_DATE,
441 P_OBJECT,
442 P_MSGLEN,
443 P_MSG,
444 } phase = P_FROM;
446 l = *line;
447 if (strncmp(l, "tag ", 4) != 0)
448 errx(1, "%s: unexpected line: %s", __func__, l);
449 l += 4;
451 fputc('{', fp);
452 json_field(fp, "type", "tag", 1);
453 json_field(fp, "tag", l, 1);
455 while (!done) {
456 if ((linelen = getline(line, linesize, stdin)) == -1)
457 break;
459 if ((*line)[linelen - 1] == '\n')
460 (*line)[--linelen] = '\0';
462 l = *line;
463 switch (phase) {
464 case P_FROM:
465 if (strncmp(l, "from: ", 6) != 0)
466 errx(1, "unexpected from line");
467 l += 6;
469 json_author(fp, "tagger", l, 1);
471 phase = P_DATE;
472 break;
474 case P_DATE:
475 /* optional */
476 if (!strncmp(l, "date: ", 6)) {
477 l += 6;
478 json_field(fp, "date", l, 1);
479 phase = P_OBJECT;
480 break;
482 phase = P_OBJECT;
483 /* fallthough */
485 case P_OBJECT:
486 /* optional */
487 if (!strncmp(l, "object: ", 8)) {
488 char *type, *id;
490 l += 8;
491 type = l;
492 id = strchr(l, ' ');
493 if (id == NULL)
494 errx(1, "malformed tag object line");
495 *id++ = '\0';
497 fputs("\"object\":{", fp);
498 json_field(fp, "type", type, 1);
499 json_field(fp, "id", id, 0);
500 fputs("},", fp);
502 phase = P_MSGLEN;
503 break;
505 phase = P_MSGLEN;
506 /* fallthrough */
508 case P_MSGLEN:
509 if (strncmp(l, "messagelen: ", 12) != 0)
510 errx(1, "unexpected messagelen line");
511 l += 12;
512 msglen = strtonum(l, 1, INT_MAX, &errstr);
513 if (errstr)
514 errx(1, "message len is %s: %s", errstr, l);
516 msglen++;
518 phase = P_MSG;
519 break;
521 case P_MSG:
522 if (*l == ' ') {
523 l++; /* skip leading space */
524 linelen--;
526 if (msgwrote == 0 && linelen != 0) {
527 fprintf(fp, "\"message\":\"");
528 escape(fp, l);
529 escape(fp, "\n");
530 msgwrote += linelen;
531 } else if (msgwrote != 0) {
532 escape(fp, l);
533 escape(fp, "\n");
536 msglen -= linelen + 1;
537 if (msglen <= 0) {
538 fprintf(fp, "\"");
539 msgwrote = 0;
540 done = 1;
541 break;
543 break;
545 default:
546 /* unreachable */
547 errx(1, "unexpected line: %s", *line);
550 if (ferror(stdin))
551 err(1, "getline");
552 if (!done)
553 errx(1, "unexpected EOF");
554 fputc('}', fp);
556 return 0;
559 static int
560 jsonify(FILE *fp)
562 char *line = NULL;
563 size_t linesize = 0;
564 ssize_t linelen;
565 int needcomma = 0;
567 fprintf(fp, "{\"notifications\":[");
568 while ((linelen = getline(&line, &linesize, stdin)) != -1) {
569 if (line[linelen - 1] == '\n')
570 line[--linelen] = '\0';
572 if (*line == '\0')
573 continue;
575 if (needcomma)
576 fputc(',', fp);
577 needcomma = 1;
579 if (strncmp(line, "Removed refs/heads/", 19) == 0) {
580 if (jsonify_branch_rm(fp, line) == -1)
581 err(1, "jsonify_branch_rm");
582 continue;
585 if (strncmp(line, "commit ", 7) == 0) {
586 if (jsonify_commit(fp, &line, &linesize) == -1)
587 err(1, "jsonify_commit");
588 continue;
591 if (*line >= '0' && *line <= '9') {
592 if (jsonify_commit_short(fp, line) == -1)
593 err(1, "jsonify_commit_short");
594 continue;
597 if (strncmp(line, "tag ", 4) == 0) {
598 if (jsonify_tag(fp, &line, &linesize) == -1)
599 err(1, "jsonify_tag");
600 continue;
603 errx(1, "unexpected line: %s", line);
605 if (ferror(stdin))
606 err(1, "getline");
607 fprintf(fp, "]}");
609 return 0;
612 static char *
613 basic_auth(const char *username, const char *password)
615 char *tmp;
616 int len;
618 len = asprintf(&tmp, "%s:%s", username, password);
619 if (len == -1)
620 err(1, "asprintf");
622 /* XXX base64-ify */
623 return tmp;
626 static inline int
627 bufio2poll(struct bufio *bio)
629 int f, ret = 0;
631 f = bufio_ev(bio);
632 if (f & BUFIO_WANT_READ)
633 ret |= POLLIN;
634 if (f & BUFIO_WANT_WRITE)
635 ret |= POLLOUT;
636 return ret;
639 int
640 main(int argc, char **argv)
642 FILE *tmpfp;
643 struct bufio bio;
644 struct pollfd pfd;
645 struct timespec timeout;
646 const char *username;
647 const char *password;
648 const char *timeoutstr;
649 const char *errstr;
650 const char *host = NULL, *port = NULL, *path = NULL;
651 char *auth, *line, *spc;
652 size_t len;
653 ssize_t r;
654 off_t paylen;
655 int tls = 0;
656 int response_code = 0, done = 0;
657 int ch, flags, ret, nonstd = 0;
659 #ifndef PROFILE
660 if (pledge("stdio rpath tmppath dns inet", NULL) == -1)
661 err(1, "pledge");
662 #endif
664 while ((ch = getopt(argc, argv, "ch:p:")) != -1) {
665 switch (ch) {
666 case 'c':
667 tls = 1;
668 break;
669 case 'h':
670 host = optarg;
671 break;
672 case 'p':
673 port = optarg;
674 break;
675 default:
676 usage();
679 argc -= optind;
680 argv += optind;
682 if (host == NULL || argc != 1)
683 usage();
684 if (tls && port == NULL)
685 port = "443";
686 path = argv[0];
688 username = getenv("GOT_NOTIFY_HTTP_USER");
689 password = getenv("GOT_NOTIFY_HTTP_PASS");
690 if ((username != NULL && password == NULL) ||
691 (username == NULL && password != NULL))
692 errx(1, "username or password are not specified");
693 if (username && *password == '\0')
694 errx(1, "password can't be empty");
696 /* used by the regression test suite */
697 timeoutstr = getenv("GOT_NOTIFY_TIMEOUT");
698 if (timeoutstr) {
699 http_timeout = strtonum(timeoutstr, 0, 600, &errstr);
700 if (errstr != NULL)
701 errx(1, "timeout in seconds is %s: %s",
702 errstr, timeoutstr);
705 memset(&timeout, 0, sizeof(timeout));
706 timeout.tv_sec = http_timeout;
708 tmpfp = got_opentemp();
709 if (tmpfp == NULL)
710 err(1, "opentemp");
712 jsonify(tmpfp);
714 paylen = ftello(tmpfp);
715 if (paylen == -1)
716 err(1, "ftello");
717 if (fseeko(tmpfp, 0, SEEK_SET) == -1)
718 err(1, "fseeko");
720 #ifndef PROFILE
721 /* drop tmppath */
722 if (pledge("stdio rpath dns inet", NULL) == -1)
723 err(1, "pledge");
724 #endif
726 memset(&pfd, 0, sizeof(pfd));
727 pfd.fd = dial(host, port);
729 if ((flags = fcntl(pfd.fd, F_GETFL)) == -1)
730 err(1, "fcntl(F_GETFL)");
731 if (fcntl(pfd.fd, F_SETFL, flags | O_NONBLOCK) == -1)
732 err(1, "fcntl(F_SETFL)");
734 if (bufio_init(&bio) == -1)
735 err(1, "bufio_init");
736 bufio_set_fd(&bio, pfd.fd);
737 if (tls && bufio_starttls(&bio, host, 0, NULL, 0, NULL, 0) == -1)
738 err(1, "bufio_starttls");
740 #ifndef PROFILE
741 /* drop rpath dns inet */
742 if (pledge("stdio", NULL) == -1)
743 err(1, "pledge");
744 #endif
746 if ((!tls && strcmp(port, "80") != 0) ||
747 (tls && strcmp(port, "443")) != 0)
748 nonstd = 1;
750 ret = bufio_compose_fmt(&bio,
751 "POST %s HTTP/1.1\r\n"
752 "Host: %s%s%s\r\n"
753 "Content-Type: application/json\r\n"
754 "Content-Length: %lld\r\n"
755 "User-Agent: %s\r\n"
756 "Connection: close\r\n",
757 path, host,
758 nonstd ? ":" : "", nonstd ? port : "",
759 (long long)paylen, USERAGENT);
760 if (ret == -1)
761 err(1, "bufio_compose_fmt");
763 if (username) {
764 auth = basic_auth(username, password);
765 ret = bufio_compose_fmt(&bio, "Authorization: basic %s\r\n",
766 auth);
767 if (ret == -1)
768 err(1, "bufio_compose_fmt");
769 free(auth);
772 if (bufio_compose(&bio, "\r\n", 2) == -1)
773 err(1, "bufio_compose");
775 while (!done) {
776 struct timespec elapsed, start, stop;
777 char buf[BUFSIZ];
779 pfd.events = bufio2poll(&bio);
780 clock_gettime(CLOCK_MONOTONIC, &start);
781 ret = ppoll(&pfd, 1, &timeout, NULL);
782 if (ret == -1)
783 err(1, "poll");
784 clock_gettime(CLOCK_MONOTONIC, &stop);
785 timespecsub(&stop, &start, &elapsed);
786 timespecsub(&timeout, &elapsed, &timeout);
787 if (ret == 0 || timeout.tv_sec <= 0)
788 errx(1, "timeout");
790 if (bio.wbuf.len > 0 && (pfd.revents & POLLOUT)) {
791 if (bufio_write(&bio) == -1 && errno != EAGAIN)
792 errx(1, "bufio_write: %s", bufio_io_err(&bio));
794 if (pfd.revents & POLLIN) {
795 r = bufio_read(&bio);
796 if (r == -1 && errno != EAGAIN)
797 errx(1, "bufio_read: %s", bufio_io_err(&bio));
798 if (r == 0)
799 errx(1, "unexpected EOF");
801 for (;;) {
802 line = buf_getdelim(&bio.rbuf, "\r\n", &len);
803 if (line == NULL)
804 break;
805 if (response_code && *line == '\0') {
806 /*
807 * end of headers, don't bother
808 * reading the body, if there is.
809 */
810 done = 1;
811 break;
813 if (response_code) {
814 buf_drain(&bio.rbuf, len);
815 continue;
817 spc = strchr(line, ' ');
818 if (spc == NULL)
819 errx(1, "bad reply");
820 *spc++ = '\0';
821 if (strcasecmp(line, "HTTP/1.1") != 0)
822 errx(1, "unexpected protocol: %s",
823 line);
824 line = spc;
826 spc = strchr(line, ' ');
827 if (spc == NULL)
828 errx(1, "bad reply");
829 *spc++ = '\0';
831 response_code = strtonum(line, 100, 599,
832 &errstr);
833 if (errstr != NULL)
834 errx(1, "response code is %s: %s",
835 errstr, line);
837 buf_drain(&bio.rbuf, len);
839 if (done)
840 break;
843 if (!feof(tmpfp) && bio.wbuf.len < sizeof(buf)) {
844 len = fread(buf, 1, sizeof(buf), tmpfp);
845 if (len == 0) {
846 if (ferror(tmpfp))
847 err(1, "fread");
848 continue;
851 if (bufio_compose(&bio, buf, len) == -1)
852 err(1, "buf_compose");
856 if (response_code >= 200 && response_code < 300)
857 return 0;
858 errx(1, "request failed with code %d", response_code);