2 * Copyright (c) 2024 Omar Polo <op@openbsd.org>
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.
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.
18 #include <sys/types.h>
19 #include <sys/socket.h>
32 #include "got_opentemp.h"
33 #include "got_version.h"
38 #define USERAGENT "got-notify-http/" GOT_VERSION_STR
40 static int http_timeout = 300; /* 5 minutes in seconds */
45 fprintf(stderr, "usage: %s [-c] -r repo -h host -p port path\n",
51 dial(const char *host, const char *port)
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);
62 errx(1, "failed to resolve %s:%s: %s", host, port,
66 for (res = res0; res; res = res->ai_next) {
67 s = socket(res->ai_family, res->ai_socktype,
74 if (connect(s, res->ai_addr, res->ai_addrlen) == -1) {
93 escape(FILE *fp, const uint8_t *s)
95 uint32_t codepoint, state;
96 const uint8_t *start = s;
100 switch (decode(&state, &codepoint, *s)) {
105 fprintf(fp, "\\%c", *s);
123 /* other control characters */
124 if (codepoint < ' ' || codepoint == 0x7F) {
125 fprintf(fp, "\\u%04x", codepoint);
128 fwrite(start, 1, s - start + 1, fp);
135 /* bad UTF-8 sequence; try to recover */
136 fputs("\\uFFFD", fp);
145 json_field(FILE *fp, const char *key, const char *val, int comma)
147 fprintf(fp, "\"%s\":\"", key);
149 fprintf(fp, "\"%s", comma ? "," : "");
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, '<');
161 /* long format, e.g. "Omar Polo <op@openbsd.org>" */
163 json_field(fp, "full", address, 1);
166 while (endname > address && endname[-1] == ' ')
170 json_field(fp, "name", address, 1);
173 lt = strchr(email, '>');
177 json_field(fp, "mail", email, 1);
179 at = strchr(email, '@');
183 json_field(fp, "user", email, 0);
185 /* short format only shows the username */
186 json_field(fp, "user", address, 0);
189 fprintf(fp, "}%s", comma ? "," : "");
193 jsonify_branch_rm(FILE *fp, char *line, const char *repo)
197 line = strchr(line, ' ');
199 errx(1, "invalid branch rm line");
200 line += strspn(line, " ");
204 line = strchr(line, ':');
206 errx(1, "invalid branch rm line");
208 id = line + strspn(line, " ");
211 json_field(fp, "type", "branch-deleted", 1);
212 json_field(fp, "repo", repo, 1);
213 json_field(fp, "ref", ref, 1);
214 json_field(fp, "id", id, 0);
221 jsonify_commit_short(FILE *fp, char *line, const char *repo)
223 char *t, *date, *id, *author, *message;
227 if ((t = strchr(t, ' ')) == NULL)
228 errx(1, "malformed line");
232 if ((t = strchr(t, ' ')) == NULL)
233 errx(1, "malformed line");
237 if ((t = strchr(t, ' ')) == NULL)
238 errx(1, "malformed line");
243 fprintf(fp, "{\"type\":\"commit\",\"short\":true,");
244 json_field(fp, "repo", repo, 1);
245 json_field(fp, "id", id, 1);
246 json_author(fp, "committer", author, 1);
247 json_field(fp, "date", date, 1);
248 json_field(fp, "short_message", message, 0);
255 jsonify_commit(FILE *fp, const char *repo, char **line, ssize_t *linesize)
263 int msglen = 0, msgwrote = 0;
278 if (strncmp(l, "commit ", 7) != 0)
279 errx(1, "%s: unexpected line: %s", __func__, l);
282 fprintf(fp, "{\"type\":\"commit\",\"short\":false,");
283 json_field(fp, "repo", repo, 1);
284 json_field(fp, "id", l, 1);
287 if ((linelen = getline(line, linesize, stdin)) == -1)
290 if ((*line)[linelen - 1] == '\n')
291 (*line)[--linelen] = '\0';
296 if (strncmp(l, "from: ", 6) != 0)
297 errx(1, "unexpected from line");
304 json_author(fp, "author", l, 1);
311 if (!strncmp(l, "via: ", 5)) {
313 json_author(fp, "committer", l, 1);
318 if (author == NULL) /* impossible */
319 err(1, "from not specified");
320 json_author(fp, "committer", author, 1);
329 if (!strncmp(l, "date: ", 6)) {
331 json_field(fp, "date", l, 1);
339 /* optional - more than one */
340 if (!strncmp(l, "parent ", 7)) {
342 l += strcspn(l, ":");
347 fprintf(fp, "\"parents\":[");
364 if (strncmp(l, "messagelen: ", 12) != 0)
365 errx(1, "unexpected messagelen line");
367 msglen = strtonum(l, 1, INT_MAX, &errstr);
369 errx(1, "message len is %s: %s", errstr, l);
378 * The commit message is indented with one extra
379 * space which is not accounted for in messagelen,
380 * but we also strip the trailing \n so that
383 * Since we read line-by-line and there is always
384 * a \n added at the end of the message,
385 * tolerate one byte less than advertised.
388 errx(1, "unexpected line in commit message");
390 l++; /* skip leading space */
393 if (msgwrote == 0 && linelen != 0) {
394 json_field(fp, "short_message", l, 1);
395 fprintf(fp, "\"message\":\"");
399 } else if (msgwrote != 0) {
404 msglen -= linelen + 1;
413 if (files == 0 && !strcmp(l, " "))
417 fputs("\"diffstat\":{\"files\":[", fp);
426 errx(1, "bad diffstat line");
435 json_field(fp, "action", "added", 1);
438 json_field(fp, "action", "deleted", 1);
441 json_field(fp, "action", "modified", 1);
444 json_field(fp, "action", "mode changed", 1);
447 json_field(fp, "action", "unknown", 1);
455 errx(1, "invalid diffstat: no filename");
460 errx(1, "invalid diffstat: no separator");
462 while (t > filename && *t == ' ')
464 json_field(fp, "file", filename, 1);
472 errx(1, "invalid diffstat: no added counter");
475 n = strtonum(l, 0, INT_MAX, &errstr);
477 errx(1, "added counter is %s: %s", errstr, l);
478 fprintf(fp, "\"added\":%d,", n);
486 errx(1, "invalid diffstat: no del counter");
489 n = strtonum(l, 0, INT_MAX, &errstr);
491 errx(1, "del counter is %s: %s", errstr, l);
492 fprintf(fp, "\"removed\":%d", n);
501 fputs("\"total\":{", fp);
506 errx(1, "missing number of additions");
509 n = strtonum(t, 0, INT_MAX, &errstr);
511 errx(1, "add counter is %s: %s", errstr, t);
512 fprintf(fp, "\"added\":%d,", n);
516 errx(1, "missing number of deletions");
523 errx(1, "malformed diffstat sum line");
526 n = strtonum(l, 0, INT_MAX, &errstr);
528 errx(1, "del counter is %s: %s", errstr, l);
529 fprintf(fp, "\"removed\":%d", n);
537 errx(1, "unexpected line: %s", *line);
543 errx(1, "unexpected EOF");
550 jsonify_tag(FILE *fp, const char *repo, char **line, ssize_t *linesize)
555 int msglen = 0, msgwrote = 0;
566 if (strncmp(l, "tag ", 4) != 0)
567 errx(1, "%s: unexpected line: %s", __func__, l);
571 json_field(fp, "type", "tag", 1);
572 json_field(fp, "repo", repo, 1);
573 json_field(fp, "tag", l, 1);
576 if ((linelen = getline(line, linesize, stdin)) == -1)
579 if ((*line)[linelen - 1] == '\n')
580 (*line)[--linelen] = '\0';
585 if (strncmp(l, "from: ", 6) != 0)
586 errx(1, "unexpected from line");
589 json_author(fp, "tagger", l, 1);
596 if (!strncmp(l, "date: ", 6)) {
598 json_field(fp, "date", l, 1);
607 if (!strncmp(l, "object: ", 8)) {
614 errx(1, "malformed tag object line");
617 fputs("\"object\":{", fp);
618 json_field(fp, "type", type, 1);
619 json_field(fp, "id", id, 0);
629 if (strncmp(l, "messagelen: ", 12) != 0)
630 errx(1, "unexpected messagelen line");
632 msglen = strtonum(l, 1, INT_MAX, &errstr);
634 errx(1, "message len is %s: %s", errstr, l);
643 errx(1, "unexpected line in tag message");
645 l++; /* skip leading space */
648 if (msgwrote == 0 && linelen != 0) {
649 fprintf(fp, "\"message\":\"");
653 } else if (msgwrote != 0) {
658 msglen -= linelen + 1;
668 errx(1, "unexpected line: %s", *line);
674 errx(1, "unexpected EOF");
681 jsonify(FILE *fp, const char *repo)
688 fprintf(fp, "{\"notifications\":[");
689 while ((linelen = getline(&line, &linesize, stdin)) != -1) {
690 if (line[linelen - 1] == '\n')
691 line[--linelen] = '\0';
700 if (strncmp(line, "Removed refs/heads/", 19) == 0) {
701 if (jsonify_branch_rm(fp, line, repo) == -1)
702 err(1, "jsonify_branch_rm");
706 if (strncmp(line, "commit ", 7) == 0) {
707 if (jsonify_commit(fp, repo, &line, &linesize) == -1)
708 err(1, "jsonify_commit");
712 if (*line >= '0' && *line <= '9') {
713 if (jsonify_commit_short(fp, line, repo) == -1)
714 err(1, "jsonify_commit_short");
718 if (strncmp(line, "tag ", 4) == 0) {
719 if (jsonify_tag(fp, repo, &line, &linesize) == -1)
720 err(1, "jsonify_tag");
724 errx(1, "unexpected line: %s", line);
734 basic_auth(const char *username, const char *password)
739 len = asprintf(&tmp, "%s:%s", username, password);
748 bufio2poll(struct bufio *bio)
753 if (f & BUFIO_WANT_READ)
755 if (f & BUFIO_WANT_WRITE)
761 main(int argc, char **argv)
766 struct timespec timeout;
767 const char *username;
768 const char *password;
769 const char *timeoutstr;
771 const char *repo = NULL;
772 const char *host = NULL, *port = NULL, *path = NULL;
773 char *auth, *line, *spc;
778 int response_code = 0, done = 0;
779 int ch, flags, ret, nonstd = 0;
782 if (pledge("stdio rpath tmppath dns inet", NULL) == -1)
786 while ((ch = getopt(argc, argv, "ch:p:r:")) != -1) {
807 if (host == NULL || repo == NULL || argc != 1)
809 if (tls && port == NULL)
813 username = getenv("GOT_NOTIFY_HTTP_USER");
814 password = getenv("GOT_NOTIFY_HTTP_PASS");
815 if ((username != NULL && password == NULL) ||
816 (username == NULL && password != NULL))
817 errx(1, "username or password are not specified");
818 if (username && *password == '\0')
819 errx(1, "password can't be empty");
821 /* used by the regression test suite */
822 timeoutstr = getenv("GOT_NOTIFY_TIMEOUT");
824 http_timeout = strtonum(timeoutstr, 0, 600, &errstr);
826 errx(1, "timeout in seconds is %s: %s",
830 memset(&timeout, 0, sizeof(timeout));
831 timeout.tv_sec = http_timeout;
833 tmpfp = got_opentemp();
837 jsonify(tmpfp, repo);
839 paylen = ftello(tmpfp);
842 if (fseeko(tmpfp, 0, SEEK_SET) == -1)
847 if (pledge("stdio rpath dns inet", NULL) == -1)
851 memset(&pfd, 0, sizeof(pfd));
852 pfd.fd = dial(host, port);
854 if ((flags = fcntl(pfd.fd, F_GETFL)) == -1)
855 err(1, "fcntl(F_GETFL)");
856 if (fcntl(pfd.fd, F_SETFL, flags | O_NONBLOCK) == -1)
857 err(1, "fcntl(F_SETFL)");
859 if (bufio_init(&bio) == -1)
860 err(1, "bufio_init");
861 bufio_set_fd(&bio, pfd.fd);
862 if (tls && bufio_starttls(&bio, host, 0, NULL, 0, NULL, 0) == -1)
863 err(1, "bufio_starttls");
866 /* drop rpath dns inet */
867 if (pledge("stdio", NULL) == -1)
871 if ((!tls && strcmp(port, "80") != 0) ||
872 (tls && strcmp(port, "443")) != 0)
875 ret = bufio_compose_fmt(&bio,
876 "POST %s HTTP/1.1\r\n"
878 "Content-Type: application/json\r\n"
879 "Content-Length: %lld\r\n"
881 "Connection: close\r\n",
883 nonstd ? ":" : "", nonstd ? port : "",
884 (long long)paylen, USERAGENT);
886 err(1, "bufio_compose_fmt");
889 auth = basic_auth(username, password);
890 ret = bufio_compose_fmt(&bio, "Authorization: basic %s\r\n",
893 err(1, "bufio_compose_fmt");
897 if (bufio_compose(&bio, "\r\n", 2) == -1)
898 err(1, "bufio_compose");
901 struct timespec elapsed, start, stop;
904 pfd.events = bufio2poll(&bio);
905 clock_gettime(CLOCK_MONOTONIC, &start);
906 ret = ppoll(&pfd, 1, &timeout, NULL);
909 clock_gettime(CLOCK_MONOTONIC, &stop);
910 timespecsub(&stop, &start, &elapsed);
911 timespecsub(&timeout, &elapsed, &timeout);
912 if (ret == 0 || timeout.tv_sec <= 0)
915 if (bio.wbuf.len > 0 && (pfd.revents & POLLOUT)) {
916 if (bufio_write(&bio) == -1 && errno != EAGAIN)
917 errx(1, "bufio_write: %s", bufio_io_err(&bio));
919 if (pfd.revents & POLLIN) {
920 r = bufio_read(&bio);
921 if (r == -1 && errno != EAGAIN)
922 errx(1, "bufio_read: %s", bufio_io_err(&bio));
924 errx(1, "unexpected EOF");
927 line = buf_getdelim(&bio.rbuf, "\r\n", &len);
930 if (response_code && *line == '\0') {
932 * end of headers, don't bother
933 * reading the body, if there is.
939 buf_drain(&bio.rbuf, len);
942 spc = strchr(line, ' ');
944 errx(1, "bad reply");
946 if (strcasecmp(line, "HTTP/1.1") != 0)
947 errx(1, "unexpected protocol: %s",
951 spc = strchr(line, ' ');
953 errx(1, "bad reply");
956 response_code = strtonum(line, 100, 599,
959 errx(1, "response code is %s: %s",
962 buf_drain(&bio.rbuf, len);
968 if (!feof(tmpfp) && bio.wbuf.len < sizeof(buf)) {
969 len = fread(buf, 1, sizeof(buf), tmpfp);
976 if (bufio_compose(&bio, buf, len) == -1)
977 err(1, "buf_compose");
981 if (response_code >= 200 && response_code < 300)
983 errx(1, "request failed with code %d", response_code);