commit - bc0cdda132594ebe68c239990867d8e9a73af778
commit + 94a3f4e9292a2c4019c2e68c242efa31f3e1fe4f
blob - 45a21b3bd385d22ec6ae514d311b7bd4a6753183
blob + 602eb98ac68d38179e233dd928e6198ad72b3e34
--- gotd/gotd.conf.5
+++ gotd/gotd.conf.5
.Cm got log -d
command.
.Pp
-.\" Notifications via HTTP require a HTTP or HTTPS server which is accepting
-.\" POST requests with or without HTTP Basic authentication.
-.\" Depending on the use case a custom server-side CGI script may be required
-.\" for the processing of notifications.
-.\" HTTP notifications can achieve functionality
-.\" similar to Git's server-side post-receive hook script with
-.\" .Xr gotd 8
-.\" by triggering arbitrary post-commit actions via the HTTP server.
-.\" .Pp
+Notifications via HTTP require a HTTP or HTTPS server which is accepting
+POST requests with or without HTTP Basic authentication.
+Depending on the use case a custom server-side CGI script may be required
+for the processing of notifications.
+HTTP notifications can achieve functionality
+similar to Git's server-side post-receive hook script with
+.Xr gotd 8
+by triggering arbitrary post-commit actions via the HTTP server.
+.Pp
The
.Ic notify
directive expects parameters which must be enclosed in curly braces.
.Ic port
directives can be used to specify a different SMTP server address and port.
.Pp
-.\" .It Ic url Ar URL Ic user Ar user Ic password Ar password Oc
-.\" Send notifications via HTTP.
-.\" This directive may be specified multiple times to build a list of
-.\" HTTP servers to send notifications to.
-.\" .Pp
-.\" The notification will be sent as a POST request to the given
-.\" .Ar URL ,
-.\" which must be a valid HTTP URL and begin with either
-.\" .Dq http://
-.\" or
-.\" .Dq https:// .
-.\" If HTTPS is used, sending of notifications will only succeed if
-.\" no TLS errors occur.
-.\" .Pp
-.\" The optional
-.\" .Ic user
-.\" and
-.\" .Ic password
-.\" directives enable HTTP Basic authentication.
-.\" If used, both a
-.\" .Ar user
-.\" and a
-.\" .Ar password
-.\" must be specified.
-.\" The
-.\" .Ar password
-.\" must not be an empty string.
-.\" .Pp
-.\" The request body contains a JSON document with the following objects:
-.\" .Bl -tag -width { "notifications" : array }
-.\" .It { "notifications" : array }
-.\" The top-level object contains an array of all notifications in this request.
-.\" .It TODO ...
-.\" .El
+.It Ic url Ar URL Oo Ic user Ar user Ic password Ar password Oc
+Send notifications via HTTP.
+This directive may be specified multiple times to build a list of
+HTTP servers to send notifications to.
+.Pp
+The notification will be sent as a POST request to the given
+.Ar URL ,
+which must be a valid HTTP URL and begin with either
+.Dq http://
+or
+.Dq https:// .
+If HTTPS is used, sending of notifications will only succeed if
+no TLS errors occur.
+.Pp
+The optional
+.Ic user
+and
+.Ic password
+directives enable HTTP Basic authentication.
+If used, both a
+.Ar user
+and a
+.Ar password
+must be specified.
+The
+.Ar password
+must not be an empty string.
+.Pp
+The request body contains a JSON document with the following objects:
+.Bl -tag -width { "notifications" : array }
+.It { "notifications" : array }
+The top-level object contains an array of all notifications in this request.
.El
+.El
.Sh FILES
.Bl -tag -width Ds -compact
.It Pa /etc/gotd.conf
blob - 6b6bbefe7995dab0aab38160a3ccd1e283250e98
blob + 8dc1ab0bddd3bfd0e0ec2e9de7cf1a1e6f77c065
--- gotd/gotd.h
+++ gotd/gotd.h
char *port;
} email;
struct {
- char *url;
+ int tls;
+ char *hostname;
+ char *port;
+ char *path;
char *user;
char *password;
} http;
blob - 7b7976993486091a9a6aba1d5a8d8aff5b886466
blob + ff4f322ea9437fc519b20c2fe716dddd1a9e70a2
--- gotd/libexec/Makefile
+++ gotd/libexec/Makefile
-# Makefile.in generated by automake 1.16.5 from Makefile.am.
-# gotd/libexec/Makefile. Generated from Makefile.in by configure.
+SUBDIR = got-notify-email got-notify-http
# Copyright (C) 1994-2021 Free Software Foundation, Inc.
blob - /dev/null
blob + f7c019a6d9614f8bbaac0c5ec29a63e69b94b5ea (mode 644)
--- /dev/null
+++ gotd/libexec/got-notify-http/Makefile
+.PATH:${.CURDIR}/../..
+.PATH:${.CURDIR}/../../../lib
+
+.include "../../../got-version.mk"
+
+PROG= got-notify-http
+SRCS= got-notify-http.c bufio.c opentemp.c pollfd.c error.c hash.c
+
+CPPFLAGS= -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib
+
+DPADD= ${LIBTLS}
+LDADD= -ltls
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + 779b64b8f926d8b54243bbffe99d9cb186659d70 (mode 644)
--- /dev/null
+++ gotd/libexec/got-notify-http/got-notify-http.c
+/*
+ * Copyright (c) 2024 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <netdb.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "got_opentemp.h"
+#include "got_version.h"
+
+#include "bufio.h"
+
+#define USERAGENT "got-notify-http/" GOT_VERSION_STR
+
+static int http_timeout = 300; /* 5 minutes in seconds */
+
+__dead static void
+usage(void)
+{
+ fprintf(stderr, "usage: %s [-c] -h host -p port path\n",
+ getprogname());
+ exit(1);
+}
+
+static int
+dial(const char *host, const char *port)
+{
+ struct addrinfo hints, *res, *res0;
+ const char *cause = NULL;
+ int s, error, save_errno;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ error = getaddrinfo(host, port, &hints, &res0);
+ if (error)
+ errx(1, "failed to resolve %s:%s: %s", host, port,
+ gai_strerror(error));
+
+ s = -1;
+ for (res = res0; res; res = res->ai_next) {
+ s = socket(res->ai_family, res->ai_socktype,
+ res->ai_protocol);
+ if (s == -1) {
+ cause = "socket";
+ continue;
+ }
+
+ if (connect(s, res->ai_addr, res->ai_addrlen) == -1) {
+ cause = "connect";
+ save_errno = errno;
+ close(s);
+ errno = save_errno;
+ s = -1;
+ continue;
+ }
+
+ break;
+ }
+
+ freeaddrinfo(res0);
+ if (s == -1)
+ err(1, "%s", cause);
+ return s;
+}
+
+static void
+escape(FILE *fp, const uint8_t *s)
+{
+ for (; *s; ++s) {
+ /*
+ * XXX: this is broken for UNICODE: we should leave
+ * the multibyte characters as-is.
+ */
+
+ if (*s >= ' ' && *s <= '~') {
+ fputc(*s, fp);
+ continue;
+ }
+
+ switch (*s) {
+ case '"':
+ case '\\':
+ fprintf(fp, "\\%c", *s);
+ break;
+ case '\b':
+ fprintf(fp, "\\b");
+ break;
+ case '\f':
+ fprintf(fp, "\\f");
+ break;
+ case '\n':
+ fprintf(fp, "\\n");
+ break;
+ case '\r':
+ fprintf(fp, "\\r");
+ break;
+ case '\t':
+ fprintf(fp, "\\t");
+ break;
+ default:
+ fprintf(fp, "\\u%04X", *s);
+ break;
+ }
+ }
+}
+
+static void
+json_field(FILE *fp, const char *key, const char *val, int comma)
+{
+ fprintf(fp, "\"%s\":\"", key);
+ escape(fp, val);
+ fprintf(fp, "\"%s", comma ? "," : "");
+}
+
+static int
+jsonify_short(FILE *fp)
+{
+ char *t, *date, *id, *author, *message;
+ char *line = NULL;
+ size_t linesize = 0;
+ ssize_t linelen;
+ int needcomma = 0;
+
+ fprintf(fp, "{\"notifications\":[");
+ while ((linelen = getline(&line, &linesize, stdin)) != -1) {
+ if (line[linelen - 1] == '\n')
+ line[--linelen] = '\0';
+
+ if (needcomma)
+ fputc(',', fp);
+ needcomma = 1;
+
+ t = line;
+ date = t;
+ if ((t = strchr(t, ' ')) == NULL)
+ errx(1, "malformed line");
+ *t++ = '\0';
+
+ id = t;
+ if ((t = strchr(t, ' ')) == NULL)
+ errx(1, "malformed line");
+ *t++ = '\0';
+
+ author = t;
+ if ((t = strchr(t, ' ')) == NULL)
+ errx(1, "malformed line");
+ *t++ = '\0';
+
+ message = t;
+
+ fprintf(fp, "{\"short\":true,");
+ json_field(fp, "id", id, 1);
+ json_field(fp, "author", author, 1);
+ json_field(fp, "date", date, 1);
+ json_field(fp, "message", message, 0);
+ fprintf(fp, "}");
+ }
+
+ if (ferror(stdin))
+ err(1, "getline");
+ free(line);
+
+ fprintf(fp, "]}");
+
+ return 0;
+}
+
+static int
+jsonify(FILE *fp)
+{
+ const char *errstr;
+ char *l;
+ char *line = NULL;
+ size_t linesize = 0;
+ ssize_t linelen;
+ int parent = 0;
+ int msglen = 0, msgwrote = 0;
+ int needcomma = 0;
+ enum {
+ P_COMMIT,
+ P_FROM,
+ P_VIA,
+ P_DATE,
+ P_PARENT,
+ P_MSGLEN,
+ P_MSG,
+ P_DST,
+ P_SUM,
+ } phase = P_COMMIT;
+
+ fprintf(fp, "{\"notifications\":[");
+ while ((linelen = getline(&line, &linesize, stdin)) != -1) {
+ if (line[linelen - 1] == '\n')
+ line[--linelen] = '\0';
+
+ l = line;
+ switch (phase) {
+ case P_COMMIT:
+ if (*line == '\0')
+ continue;
+
+ if (strncmp(l, "commit ", 7) != 0)
+ errx(1, "unexpected commit line: %s", line);
+ l += 7;
+
+ if (needcomma)
+ fputc(',', fp);
+ needcomma = 1;
+
+ fprintf(fp, "{\"short\":false,");
+ json_field(fp, "id", l, 1);
+
+ phase = P_FROM;
+ break;
+
+ case P_FROM:
+ if (strncmp(l, "from: ", 6) != 0)
+ errx(1, "unexpected from line");
+ l += 6;
+ json_field(fp, "author", l, 1);
+ phase = P_VIA;
+ break;
+
+ case P_VIA:
+ /* optional */
+ if (!strncmp(l, "via: ", 5)) {
+ l += 5;
+ json_field(fp, "via", l, 1);
+ phase = P_DATE;
+ break;
+ }
+ phase = P_DATE;
+ /* fallthrough */
+
+ case P_DATE:
+ /* optional */
+ if (!strncmp(l, "date: ", 6)) {
+ l += 6;
+ json_field(fp, "date", l, 1);
+ phase = P_PARENT;
+ break;
+ }
+ phase = P_PARENT;
+ /* fallthough */
+
+ case P_PARENT:
+ /* optional - more than one */
+ if (!strncmp(l, "parent ", 7)) {
+ l += 7;
+ l += strcspn(l, ":");
+ l += strspn(l, " ");
+
+ if (parent == 0) {
+ parent = 1;
+ fprintf(fp, "\"parents\":[");
+ }
+
+ fputc('"', fp);
+ escape(fp, l);
+ fputc('"', fp);
+
+ break;
+ }
+ if (parent != 0) {
+ fprintf(fp, "],");
+ parent = 0;
+ }
+ phase = P_MSGLEN;
+ /* fallthrough */
+
+ case P_MSGLEN:
+ if (strncmp(l, "messagelen: ", 12) != 0)
+ errx(1, "unexpected messagelen line");
+ l += 12;
+ msglen = strtonum(l, 1, INT_MAX, &errstr);
+ if (errstr)
+ errx(1, "message len is %s: %s", errstr, l);
+
+ fprintf(fp, "\"message\":\"");
+ phase = P_MSG;
+ break;
+
+ case P_MSG:
+ /*
+ * The commit message is indented with one extra
+ * space which is not accounted for in messagelen,
+ * but we also strip the trailing \n so that
+ * accounts for it.
+ *
+ * Since we read line-by-line and there is always
+ * a \n added at the end of the message,
+ * tolerate one byte less than advertised.
+ */
+ if (*l == ' ') {
+ escape(fp, l + 1); /* skip leading space */
+
+ /* avoid pre-pending \n to the commit msg */
+ msgwrote += linelen - 1;
+ if (msgwrote != 0)
+ escape(fp, "\n");
+ }
+ msglen -= linelen;
+ if (msglen <= 1) {
+ fprintf(fp, "\",");
+ msgwrote = 0;
+ phase = P_DST;
+ }
+ break;
+
+ case P_DST:
+ /* XXX: ignore the diffstat for now */
+ if (*line == '\0') {
+ fprintf(fp, "\"diffstat\":{},");
+ phase = P_SUM;
+ break;
+ }
+ break;
+
+ case P_SUM:
+ /* XXX: ignore the sum of changes for now */
+ fprintf(fp, "\"changes\":{}}");
+ /* restart the state machine */
+ phase = P_COMMIT;
+ break;
+
+ default:
+ errx(1, "unimplemented");
+ }
+ }
+ if (ferror(stdin))
+ err(1, "getline");
+ if (phase != P_COMMIT)
+ errx(1, "unexpected EOF");
+ free(line);
+
+ fprintf(fp, "]}");
+
+ return 0;
+}
+
+static char *
+basic_auth(const char *username, const char *password)
+{
+ char *tmp;
+ int len;
+
+ len = asprintf(&tmp, "%s:%s", username, password);
+ if (len == -1)
+ err(1, "asprintf");
+
+ /* XXX base64-ify */
+ return tmp;
+}
+
+static inline int
+bufio2poll(struct bufio *bio)
+{
+ int f, ret = 0;
+
+ f = bufio_ev(bio);
+ if (f & BUFIO_WANT_READ)
+ ret |= POLLIN;
+ if (f & BUFIO_WANT_WRITE)
+ ret |= POLLOUT;
+ return ret;
+}
+
+int
+main(int argc, char **argv)
+{
+ FILE *tmpfp;
+ struct bufio bio;
+ struct pollfd pfd;
+ struct timespec timeout;
+ const char *username;
+ const char *password;
+ const char *timeoutstr;
+ const char *errstr;
+ const char *host = NULL, *port = NULL, *path = NULL;
+ char *auth, *line, *spc;
+ size_t len;
+ ssize_t r;
+ off_t paylen;
+ int tls = 0;
+ int response_code = 0, done = 0;
+ int ch, flags, ret, nonstd = 0;
+
+#ifndef PROFILE
+ if (pledge("stdio rpath tmppath dns inet", NULL) == -1)
+ err(1, "pledge");
+#endif
+
+ while ((ch = getopt(argc, argv, "ch:p:")) != -1) {
+ switch (ch) {
+ case 'c':
+ tls = 1;
+ break;
+ case 'h':
+ host = optarg;
+ break;
+ case 'p':
+ port = optarg;
+ break;
+ default:
+ usage();
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (host == NULL || argc != 1)
+ usage();
+ if (tls && port == NULL)
+ port = "443";
+ path = argv[0];
+
+ username = getenv("GOT_NOTIFY_HTTP_USER");
+ password = getenv("GOT_NOTIFY_HTTP_PASS");
+ if ((username != NULL && password == NULL) ||
+ (username == NULL && password != NULL))
+ errx(1, "username or password are not specified");
+ if (username && *password == '\0')
+ errx(1, "password can't be empty");
+
+ /* used by the regression test suite */
+ timeoutstr = getenv("GOT_NOTIFY_TIMEOUT");
+ if (timeoutstr) {
+ http_timeout = strtonum(timeoutstr, 0, 600, &errstr);
+ if (errstr != NULL)
+ errx(1, "timeout in seconds is %s: %s",
+ errstr, timeoutstr);
+ }
+
+ memset(&timeout, 0, sizeof(timeout));
+ timeout.tv_sec = http_timeout;
+
+ tmpfp = got_opentemp();
+ if (tmpfp == NULL)
+ err(1, "opentemp");
+
+ /* detect the format of the input */
+ ch = fgetc(stdin);
+ if (ch == EOF)
+ errx(1, "unexpected EOF");
+ ungetc(ch, stdin);
+ if (ch == 'c') {
+ /* starts with "commit" so it's the long format */
+ jsonify(tmpfp);
+ } else {
+ /* starts with the date so it's the short format */
+ jsonify_short(tmpfp);
+ }
+
+ paylen = ftello(tmpfp);
+ if (paylen == -1)
+ err(1, "ftello");
+ if (fseeko(tmpfp, 0, SEEK_SET) == -1)
+ err(1, "fseeko");
+
+#ifndef PROFILE
+ /* drop tmppath */
+ if (pledge("stdio rpath dns inet", NULL) == -1)
+ err(1, "pledge");
+#endif
+
+ memset(&pfd, 0, sizeof(pfd));
+ pfd.fd = dial(host, port);
+
+ if ((flags = fcntl(pfd.fd, F_GETFL)) == -1)
+ err(1, "fcntl(F_GETFL)");
+ if (fcntl(pfd.fd, F_SETFL, flags | O_NONBLOCK) == -1)
+ err(1, "fcntl(F_SETFL)");
+
+ if (bufio_init(&bio) == -1)
+ err(1, "bufio_init");
+ bufio_set_fd(&bio, pfd.fd);
+ if (tls && bufio_starttls(&bio, host, 0, NULL, 0, NULL, 0) == -1)
+ err(1, "bufio_starttls");
+
+#ifndef PROFILE
+ /* drop rpath dns inet */
+ if (pledge("stdio", NULL) == -1)
+ err(1, "pledge");
+#endif
+
+ if ((!tls && strcmp(port, "80") != 0) ||
+ (tls && strcmp(port, "443")) != 0)
+ nonstd = 1;
+
+ ret = bufio_compose_fmt(&bio,
+ "POST %s HTTP/1.1\r\n"
+ "Host: %s%s%s\r\n"
+ "Content-Type: application/json\r\n"
+ "Content-Length: %lld\r\n"
+ "User-Agent: %s\r\n"
+ "Connection: close\r\n",
+ path, host,
+ nonstd ? ":" : "", nonstd ? port : "",
+ (long long)paylen, USERAGENT);
+ if (ret == -1)
+ err(1, "bufio_compose_fmt");
+
+ if (username) {
+ auth = basic_auth(username, password);
+ ret = bufio_compose_fmt(&bio, "Authorization: basic %s\r\n",
+ auth);
+ if (ret == -1)
+ err(1, "bufio_compose_fmt");
+ free(auth);
+ }
+
+ if (bufio_compose(&bio, "\r\n", 2) == -1)
+ err(1, "bufio_compose");
+
+ while (!done) {
+ struct timespec elapsed, start, stop;
+ char buf[BUFSIZ];
+
+ pfd.events = bufio2poll(&bio);
+ clock_gettime(CLOCK_MONOTONIC, &start);
+ ret = ppoll(&pfd, 1, &timeout, NULL);
+ if (ret == -1)
+ err(1, "poll");
+ clock_gettime(CLOCK_MONOTONIC, &stop);
+ timespecsub(&stop, &start, &elapsed);
+ timespecsub(&timeout, &elapsed, &timeout);
+ if (ret == 0 || timeout.tv_sec <= 0)
+ errx(1, "timeout");
+
+ if (bio.wbuf.len > 0 && (pfd.revents & POLLOUT)) {
+ if (bufio_write(&bio) == -1 && errno != EAGAIN)
+ errx(1, "bufio_write: %s", bufio_io_err(&bio));
+ }
+ if (pfd.revents & POLLIN) {
+ r = bufio_read(&bio);
+ if (r == -1 && errno != EAGAIN)
+ errx(1, "bufio_read: %s", bufio_io_err(&bio));
+ if (r == 0)
+ errx(1, "unexpected EOF");
+
+ for (;;) {
+ line = buf_getdelim(&bio.rbuf, "\r\n", &len);
+ if (line == NULL)
+ break;
+ if (response_code && *line == '\0') {
+ /*
+ * end of headers, don't bother
+ * reading the body, if there is.
+ */
+ done = 1;
+ break;
+ }
+ if (response_code) {
+ buf_drain(&bio.rbuf, len);
+ continue;
+ }
+ spc = strchr(line, ' ');
+ if (spc == NULL)
+ errx(1, "bad reply");
+ *spc++ = '\0';
+ if (strcasecmp(line, "HTTP/1.1") != 0)
+ errx(1, "unexpected protocol: %s",
+ line);
+ line = spc;
+
+ spc = strchr(line, ' ');
+ if (spc == NULL)
+ errx(1, "bad reply");
+ *spc++ = '\0';
+
+ response_code = strtonum(line, 100, 599,
+ &errstr);
+ if (errstr != NULL)
+ errx(1, "response code is %s: %s",
+ errstr, line);
+
+ buf_drain(&bio.rbuf, len);
+ }
+ if (done)
+ break;
+ }
+
+ if (!feof(tmpfp) && bio.wbuf.len < sizeof(buf)) {
+ len = fread(buf, 1, sizeof(buf), tmpfp);
+ if (len == 0) {
+ if (ferror(tmpfp))
+ err(1, "fread");
+ continue;
+ }
+
+ if (bufio_compose(&bio, buf, len) == -1)
+ err(1, "buf_compose");
+ }
+ }
+
+ if (response_code >= 200 && response_code < 400)
+ return 0;
+ errx(1, "request failed with code %d", response_code);
+}
blob - 0e5ef4b6a55f05b71ea22e71c322d78a16d885e2
blob + 0cb6b9561ce3a96bb5682b9905a6b4e50de169a3
--- gotd/notify.c
+++ gotd/notify.c
}
static void
-notify_http(struct gotd_notification_target *target, const char *subject_line,
- int fd)
+notify_http(struct gotd_notification_target *target, int fd)
{
- const char *argv[10] = { 0 }; /* TODO */
+ const char *argv[8];
+ int argc = 0;
+ argv[argc++] = GOTD_PATH_PROG_NOTIFY_HTTP;
+ if (target->conf.http.tls)
+ argv[argc++] = "-c";
+
+ argv[argc++] = "-h";
+ argv[argc++] = target->conf.http.hostname;
+ argv[argc++] = "-p";
+ argv[argc++] = target->conf.http.port;
+
+ argv[argc++] = target->conf.http.path;
+
+ argv[argc] = NULL;
+
run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd);
}
notify_email(target, inotify.subject_line, fd);
break;
case GOTD_NOTIFICATION_VIA_HTTP:
- notify_http(target, inotify.subject_line, fd);
+ notify_http(target, fd);
break;
}
}
blob - 4f950c38b8d4c9978a1fe92cc44aecade142422c
blob + a770e3644a96576c184dc3bcd0d7139a55e5c804
--- gotd/parse.y
+++ gotd/parse.y
{
const struct got_error *error;
struct gotd_notification_target *target;
- char *proto, *host, *port, *request_path;
- int ret = 0;
+ char *proto, *hostname, *port, *path;
+ int tls = 0, ret = 0;
- error = gotd_parse_url(&proto, &host, &port, &request_path, url);
+ error = gotd_parse_url(&proto, &hostname, &port, &path, url);
if (error) {
yyerror("invalid HTTP notification URL '%s' in "
"repository '%s': %s", url, repo->name, error->msg);
return -1;
}
+ tls = !strcmp(proto, "https");
+
if (strcmp(proto, "http") != 0 && strcmp(proto, "https") != 0) {
yyerror("invalid protocol '%s' in notification URL '%s' in "
"repository '%s", proto, url, repo->name);
+ ret = -1;
+ goto done;
+ }
+
+ if ((user != NULL && password == NULL) ||
+ (user == NULL && password != NULL)) {
+ yyerror("missing username or password");
ret = -1;
goto done;
}
STAILQ_FOREACH(target, &repo->notification_targets, entry) {
if (target->type != GOTD_NOTIFICATION_VIA_HTTP)
continue;
- if (strcmp(target->conf.http.url, url) == 0) {
+ if (target->conf.http.tls == tls &&
+ !strcmp(target->conf.http.hostname, hostname) &&
+ !strcmp(target->conf.http.port, port) &&
+ !strcmp(target->conf.http.path, path)) {
yyerror("duplicate notification for URL '%s' in "
"repository '%s'", url, repo->name);
ret = -1;
if (target == NULL)
fatal("calloc");
target->type = GOTD_NOTIFICATION_VIA_HTTP;
- target->conf.http.url = strdup(url);
- if (target->conf.http.url == NULL)
- fatal("calloc");
+ target->conf.http.tls = tls;
+ target->conf.http.hostname = hostname;
+ target->conf.http.port = port;
+ target->conf.http.path = path;
+ hostname = port = path = NULL;
+
if (user) {
target->conf.http.user = strdup(user);
if (target->conf.http.user == NULL)
- fatal("calloc");
- }
- if (password) {
+ fatal("strdup");
target->conf.http.password = strdup(password);
if (target->conf.http.password == NULL)
- fatal("calloc");
+ fatal("strdup");
}
STAILQ_INSERT_TAIL(&repo->notification_targets, target, entry);
done:
free(proto);
- free(host);
+ free(hostname);
free(port);
- free(request_path);
+ free(path);
return ret;
}
blob - /dev/null
blob + 827a113ec3eaa7465036408230a7a7c14d55577b (mode 644)
--- /dev/null
+++ lib/bufio.c
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <tls.h>
+#include <unistd.h>
+
+#include "bufio.h"
+
+int
+buf_init(struct buf *buf)
+{
+ const size_t cap = BIO_CHUNK;
+
+ memset(buf, 0, sizeof(*buf));
+ if ((buf->buf = malloc(cap)) == NULL)
+ return (-1);
+ buf->cap = cap;
+ return (0);
+}
+
+static int
+buf_grow(struct buf *buf)
+{
+ size_t newcap;
+ void *t;
+
+ newcap = buf->cap + BIO_CHUNK;
+ t = realloc(buf->buf, newcap);
+ if (t == NULL)
+ return (-1);
+ buf->buf = t;
+ buf->cap = newcap;
+ return (0);
+}
+
+int
+buf_has_line(struct buf *buf, const char *nl)
+{
+ return (memmem(buf->buf, buf->len, nl, strlen(nl)) != NULL);
+}
+
+char *
+buf_getdelim(struct buf *buf, const char *nl, size_t *len)
+{
+ uint8_t *endl;
+ size_t nlen;
+
+ *len = 0;
+
+ nlen = strlen(nl);
+ if ((endl = memmem(buf->buf, buf->len, nl, nlen)) == NULL)
+ return (NULL);
+ *len = endl + nlen - buf->buf;
+ *endl = '\0';
+ return (buf->buf);
+}
+
+void
+buf_drain(struct buf *buf, size_t l)
+{
+ buf->cur = 0;
+
+ if (l >= buf->len) {
+ buf->len = 0;
+ return;
+ }
+
+ memmove(buf->buf, buf->buf + l, buf->len - l);
+ buf->len -= l;
+}
+
+void
+buf_drain_line(struct buf *buf, const char *nl)
+{
+ uint8_t *endln;
+ size_t nlen;
+
+ nlen = strlen(nl);
+ if ((endln = memmem(buf->buf, buf->len, nl, nlen)) == NULL)
+ return;
+ buf_drain(buf, endln + nlen - buf->buf);
+}
+
+void
+buf_free(struct buf *buf)
+{
+ free(buf->buf);
+ memset(buf, 0, sizeof(*buf));
+}
+
+int
+bufio_init(struct bufio *bio)
+{
+ memset(bio, 0, sizeof(*bio));
+ bio->fd = -1;
+
+ if (buf_init(&bio->wbuf) == -1)
+ return (-1);
+ if (buf_init(&bio->rbuf) == -1) {
+ buf_free(&bio->wbuf);
+ return (-1);
+ }
+ return (0);
+}
+
+void
+bufio_free(struct bufio *bio)
+{
+ if (bio->ctx)
+ tls_free(bio->ctx);
+ bio->ctx = NULL;
+
+ if (bio->fd != -1)
+ close(bio->fd);
+ bio->fd = -1;
+
+ buf_free(&bio->rbuf);
+ buf_free(&bio->wbuf);
+}
+
+int
+bufio_close(struct bufio *bio)
+{
+ if (bio->ctx == NULL)
+ return (0);
+
+ switch (tls_close(bio->ctx)) {
+ case 0:
+ return 0;
+ case TLS_WANT_POLLIN:
+ errno = EAGAIN;
+ bio->wantev = BUFIO_WANT_READ;
+ return (-1);
+ case TLS_WANT_POLLOUT:
+ errno = EAGAIN;
+ bio->wantev = BUFIO_WANT_WRITE;
+ return (-1);
+ default:
+ return (-1);
+ }
+}
+
+int
+bufio_reset(struct bufio *bio)
+{
+ bufio_free(bio);
+ return (bufio_init(bio));
+}
+
+void
+bufio_set_fd(struct bufio *bio, int fd)
+{
+ bio->fd = fd;
+}
+
+int
+bufio_starttls(struct bufio *bio, const char *host, int insecure,
+ const uint8_t *cert, size_t certlen, const uint8_t *key, size_t keylen)
+{
+ struct tls_config *conf;
+
+ if ((conf = tls_config_new()) == NULL)
+ return (-1);
+
+ if (insecure) {
+ tls_config_insecure_noverifycert(conf);
+ tls_config_insecure_noverifyname(conf);
+ tls_config_insecure_noverifytime(conf);
+ }
+
+ if (cert && tls_config_set_keypair_mem(conf, cert, certlen,
+ key, keylen) == -1) {
+ tls_config_free(conf);
+ return (-1);
+ }
+
+ if ((bio->ctx = tls_client()) == NULL) {
+ tls_config_free(conf);
+ return (-1);
+ }
+
+ if (tls_configure(bio->ctx, conf) == -1) {
+ tls_config_free(conf);
+ return (-1);
+ }
+
+ tls_config_free(conf);
+
+ if (tls_connect_socket(bio->ctx, bio->fd, host) == -1)
+ return (-1);
+
+ return (0);
+}
+
+int
+bufio_ev(struct bufio *bio)
+{
+ short ev;
+
+ if (bio->wantev)
+ return (bio->wantev);
+
+ ev = BUFIO_WANT_READ;
+ if (bio->wbuf.len != 0)
+ ev |= BUFIO_WANT_WRITE;
+
+ return (ev);
+}
+
+int
+bufio_handshake(struct bufio *bio)
+{
+ if (bio->ctx == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+
+ switch (tls_handshake(bio->ctx)) {
+ case 0:
+ return (0);
+ case TLS_WANT_POLLIN:
+ errno = EAGAIN;
+ bio->wantev = BUFIO_WANT_READ;
+ return (-1);
+ case TLS_WANT_POLLOUT:
+ errno = EAGAIN;
+ bio->wantev = BUFIO_WANT_WRITE;
+ return (-1);
+ default:
+ return (-1);
+ }
+}
+
+ssize_t
+bufio_read(struct bufio *bio)
+{
+ struct buf *rbuf = &bio->rbuf;
+ ssize_t r;
+
+ assert(rbuf->cap >= rbuf->len);
+ if (rbuf->cap - rbuf->len < BIO_CHUNK) {
+ if (buf_grow(rbuf) == -1)
+ return (-1);
+ }
+
+ if (bio->ctx) {
+ r = tls_read(bio->ctx, rbuf->buf + rbuf->len,
+ rbuf->cap - rbuf->len);
+ switch (r) {
+ case TLS_WANT_POLLIN:
+ errno = EAGAIN;
+ bio->wantev = BUFIO_WANT_READ;
+ return (-1);
+ case TLS_WANT_POLLOUT:
+ errno = EAGAIN;
+ bio->wantev = BUFIO_WANT_WRITE;
+ return (-1);
+ case -1:
+ return (-1);
+ default:
+ bio->wantev = 0;
+ rbuf->len += r;
+ return (r);
+ }
+ }
+
+ r = read(bio->fd, rbuf->buf + rbuf->len, rbuf->cap - rbuf->len);
+ if (r == -1)
+ return (-1);
+ rbuf->len += r;
+ return (r);
+}
+
+size_t
+bufio_drain(struct bufio *bio, void *d, size_t len)
+{
+ struct buf *rbuf = &bio->rbuf;
+
+ if (len > rbuf->len)
+ len = rbuf->len;
+ memcpy(d, rbuf->buf, len);
+ buf_drain(rbuf, len);
+ return (len);
+}
+
+ssize_t
+bufio_write(struct bufio *bio)
+{
+ struct buf *wbuf = &bio->wbuf;
+ ssize_t w;
+
+ if (bio->ctx) {
+ switch (w = tls_write(bio->ctx, wbuf->buf, wbuf->len)) {
+ case TLS_WANT_POLLIN:
+ errno = EAGAIN;
+ bio->wantev = BUFIO_WANT_READ;
+ return (-1);
+ case TLS_WANT_POLLOUT:
+ errno = EAGAIN;
+ bio->wantev = BUFIO_WANT_WRITE;
+ return (-1);
+ case -1:
+ return (-1);
+ default:
+ bio->wantev = 0;
+ buf_drain(wbuf, w);
+ return (w);
+ }
+ }
+
+ w = write(bio->fd, wbuf->buf, wbuf->len);
+ if (w == -1)
+ return (-1);
+ buf_drain(wbuf, w);
+ return (w);
+}
+
+const char *
+bufio_io_err(struct bufio *bio)
+{
+ if (bio->ctx)
+ return tls_error(bio->ctx);
+
+ return strerror(errno);
+}
+
+int
+bufio_compose(struct bufio *bio, const void *d, size_t len)
+{
+ struct buf *wbuf = &bio->wbuf;
+
+ while (wbuf->cap - wbuf->len < len) {
+ if (buf_grow(wbuf) == -1)
+ return (-1);
+ }
+
+ memcpy(wbuf->buf + wbuf->len, d, len);
+ wbuf->len += len;
+ return (0);
+}
+
+int
+bufio_compose_str(struct bufio *bio, const char *str)
+{
+ return (bufio_compose(bio, str, strlen(str)));
+}
+
+int
+bufio_compose_fmt(struct bufio *bio, const char *fmt, ...)
+{
+ va_list ap;
+ char *str;
+ int r;
+
+ va_start(ap, fmt);
+ r = vasprintf(&str, fmt, ap);
+ va_end(ap);
+
+ if (r == -1)
+ return (-1);
+ r = bufio_compose(bio, str, r);
+ free(str);
+ return (r);
+}
+
+void
+bufio_rewind_cursor(struct bufio *bio)
+{
+ bio->rbuf.cur = 0;
+}
+
+int
+bufio_get_cb(void *d)
+{
+ struct bufio *bio = d;
+ struct buf *rbuf = &bio->rbuf;
+
+ if (rbuf->cur >= rbuf->len)
+ return (EOF);
+ return (rbuf->buf[rbuf->cur++]);
+}
+
+int
+bufio_peek_cb(void *d)
+{
+ struct bufio *bio = d;
+ struct buf *rbuf = &bio->rbuf;
+
+ if (rbuf->cur >= rbuf->len)
+ return (EOF);
+ return (rbuf->buf[rbuf->cur]);
+}
blob - /dev/null
blob + c51a1017d3a424e3dc4aecc1184a9d9b4cc9636b (mode 644)
--- /dev/null
+++ lib/bufio.h
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+struct tls;
+
+#define BIO_CHUNK 128
+struct buf {
+ uint8_t *buf;
+ size_t len;
+ size_t cap;
+ size_t cur;
+};
+
+struct bufio {
+ int fd;
+ struct tls *ctx;
+ int wantev;
+ struct buf wbuf;
+ struct buf rbuf;
+};
+
+#define BUFIO_WANT_READ 0x1
+#define BUFIO_WANT_WRITE 0x2
+
+int buf_init(struct buf *);
+int buf_has_line(struct buf *, const char *);
+char *buf_getdelim(struct buf *, const char *, size_t *);
+void buf_drain(struct buf *, size_t);
+void buf_drain_line(struct buf *, const char *);
+void buf_free(struct buf *);
+
+int bufio_init(struct bufio *);
+void bufio_free(struct bufio *);
+int bufio_close(struct bufio *);
+int bufio_reset(struct bufio *);
+void bufio_set_fd(struct bufio *, int);
+int bufio_starttls(struct bufio *, const char *, int,
+ const uint8_t *, size_t, const uint8_t *, size_t);
+int bufio_ev(struct bufio *);
+int bufio_handshake(struct bufio *);
+ssize_t bufio_read(struct bufio *);
+size_t bufio_drain(struct bufio *, void *, size_t);
+ssize_t bufio_write(struct bufio *);
+const char *bufio_io_err(struct bufio *);
+int bufio_compose(struct bufio *, const void *, size_t);
+int bufio_compose_str(struct bufio *, const char *);
+int bufio_compose_fmt(struct bufio *, const char *, ...)
+ __attribute__((__format__ (printf, 2, 3)));
+void bufio_rewind_cursor(struct bufio *);
+
+/* callbacks for pdjson */
+int bufio_get_cb(void *);
+int bufio_peek_cb(void *);
blob - eaca641d2887c0f5e66a410257eaf3fae004a13d
blob + 00f69c40c6fd31670fde808184134e4ac98ce8e1
--- regress/gotd/Makefile
+++ regress/gotd/Makefile
test_repo_read_denied_user test_repo_read_denied_group \
test_repo_read_bad_user test_repo_read_bad_group \
test_repo_write test_repo_write_empty test_request_bad \
- test_repo_write_protected test_email_notification
+ test_repo_write_protected test_email_notification \
+ test_http_notification
NOOBJ=Yes
CLEANFILES=gotd.conf
GOTD_TEST_REPO_NAME=test-repo
GOTD_TEST_REPO_URL=ssh://${GOTD_DEVUSER}@127.0.0.1/$(GOTD_TEST_REPO_NAME)
GOTD_TEST_SMTP_PORT=2525
+GOTD_TEST_HTTP_PORT=8000
GOTD_TEST_USER?=${DOAS_USER}
.if empty(GOTD_TEST_USER)
GOTD_DEVUSER=$(GOTD_DEVUSER) \
GOTD_USER=$(GOTD_USER) \
GOTD_TEST_SMTP_PORT=$(GOTD_TEST_SMTP_PORT) \
+ GOTD_TEST_HTTP_PORT=$(GOTD_TEST_HTTP_PORT) \
HOME=$(GOTD_TEST_USER_HOME) \
PATH=$(GOTD_TEST_USER_HOME)/bin:$(PATH)
@$(GOTD_TRAP); $(GOTD_START_CMD)
@$(GOTD_TRAP); sleep .5
+start_gotd_http_notification: ensure_root
+ @echo 'listen on "$(GOTD_SOCK)"' > $(PWD)/gotd.conf
+ @echo "user $(GOTD_USER)" >> $(PWD)/gotd.conf
+ @echo 'repository "test-repo" {' >> $(PWD)/gotd.conf
+ @echo ' path "$(GOTD_TEST_REPO)"' >> $(PWD)/gotd.conf
+ @echo ' permit rw $(GOTD_DEVUSER)' >> $(PWD)/gotd.conf
+ @echo ' notify {' >> $(PWD)/gotd.conf
+ @echo ' url "http://localhost:${GOTD_TEST_HTTP_PORT}/"' >> $(PWD)/gotd.conf
+ @echo " }" >> $(PWD)/gotd.conf
+ @echo "}" >> $(PWD)/gotd.conf
+ @$(GOTD_TRAP); $(GOTD_START_CMD)
+ @$(GOTD_TRAP); sleep .5
+
prepare_test_repo: ensure_root
@chown ${GOTD_USER} "${GOTD_TEST_REPO}"
@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./prepare_test_repo.sh'
'env $(GOTD_TEST_ENV) sh ./email_notification.sh'
@$(GOTD_STOP_CMD) 2>/dev/null
+test_http_notification: prepare_test_repo start_gotd_http_notification
+ @-$(GOTD_TRAP); su -m ${GOTD_TEST_USER} -c \
+ 'env $(GOTD_TEST_ENV) sh ./http_notification.sh'
+ @$(GOTD_STOP_CMD) 2>/dev/null
+
.include <bsd.regress.mk>
blob - /dev/null
blob + 7dbafb4ae773efae234479b2c918d895dce5ac1e (mode 755)
--- /dev/null
+++ regress/gotd/http-server
+#!/usr/bin/env perl
+#
+# Copyright (c) 2024 Omar Polo <op@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use v5.36;
+use IPC::Open2;
+use Getopt::Long qw(:config bundling);
+
+my $port = 8000;
+
+GetOptions("p:i" => \$port)
+ or die("usage: $0 [-p port]\n");
+
+my $pid = open2(my $out, my $in, 'nc', '-l', 'localhost', $port);
+
+my $clen;
+while (<$out>) {
+ local $/ = "\r\n";
+
+ chomp;
+ say;
+
+ last if /^$/;
+
+ if (m/^POST/) {
+ die "bad http request" unless m,^POST / HTTP/1.1$,;
+ next;
+ }
+
+ if (m/^Host:/) {
+ die "bad Host header" unless /^Host: localhost:$port$/;
+ next;
+ }
+
+ if (m/^Content-Type/) {
+ die "bad content-type header"
+ unless m,Content-Type: application/json$,;
+ next;
+ }
+
+ if (m/^Content-Length/) {
+ die "double content-length" if defined $clen;
+ die "bad content-length header"
+ unless m/Content-Length: (\d+)$/;
+ $clen = $1;
+ next;
+ }
+
+ if (m/Connection/) {
+ die "bad connection header"
+ unless m/Connection: close$/;
+ next;
+ }
+}
+
+die "no Content-Length header" unless defined $clen;
+
+while ($clen != 0) {
+ my $len = $clen;
+ $len = 512 if $clen > 512;
+
+ my $r = read($out, my $buf, $len);
+ $clen -= $r;
+
+ print $buf;
+}
+say "";
+
+print $in "HTTP/1.1 200 OK\r\n";
+print $in "Content-Length: 0\r\n";
+print $in "Connection: close\r\n";
+print $in "\r\n";
+
+close $in;
+close $out;
+
+waitpid($pid, 0);
+exit $? >> 8;
blob - /dev/null
blob + 5bd5779ff7fd7e13571b71c414ee5f1bb1f6dc2e (mode 644)
--- /dev/null
+++ regress/gotd/http_notification.sh
+#!/bin/sh
+#
+# Copyright (c) 2024 Omar Polo <op@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ../cmdline/common.sh
+. ./common.sh
+
+test_file_changed() {
+ local testroot=`test_init file_changed 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got checkout -q $testroot/repo-clone $testroot/wt >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ echo "change alpha" > $testroot/wt/alpha
+ (cd $testroot/wt && got commit -m 'make changes' > /dev/null)
+ local commit_id=`git_show_head $testroot/repo-clone`
+ local author_time=`git_show_author_time $testroot/repo-clone`
+
+ timeout 5 ./http-server -p $GOTD_TEST_HTTP_PORT \
+ > $testroot/stdout &
+
+ got send -b main -q -r $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ wait %1 # wait for the http "server"
+
+ d=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+
+ cat <<-EOF > $testroot/stdout.expected
+ POST / HTTP/1.1
+ Host: localhost:${GOTD_TEST_HTTP_PORT}
+ Content-Type: application/json
+ Content-Length: 224
+ User-Agent: got-notify-http/${GOT_VERSION_STR}
+ Connection: close
+
+ {"notifications":[{"short":false,"id":"$commit_id","author":"$GOT_AUTHOR","date":"$d","message":"make changes\n","diffstat":{},"changes":{}}]}
+ EOF
+
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$ret"
+}
+
+test_many_commits_not_summarized() {
+ local testroot=`test_init many_commits_not_summarized 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got checkout -q $testroot/repo-clone $testroot/wt >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ for i in `seq 1 24`; do
+ echo "alpha $i" > $testroot/wt/alpha
+ (cd $testroot/wt && got commit -m 'make changes' > /dev/null)
+ local commit_id=`git_show_head $testroot/repo-clone`
+ local author_time=`git_show_author_time $testroot/repo-clone`
+ d=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+ set -- "$@" "$commit_id $d"
+ done
+
+ timeout 5 ./http-server -p "$GOTD_TEST_HTTP_PORT" \
+ > $testroot/stdout &
+
+ got send -b main -q -r $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ wait %1 # wait for the http "server"
+
+ cat <<-EOF > $testroot/stdout.expected
+ POST / HTTP/1.1
+ Host: localhost:${GOTD_TEST_HTTP_PORT}
+ Content-Type: application/json
+ Content-Length: 4939
+ User-Agent: got-notify-http/${GOT_VERSION_STR}
+ Connection: close
+
+ EOF
+
+ printf '{"notifications":[' >> $testroot/stdout.expected
+ comma=""
+ for i in `seq 1 24`; do
+ s=`pop_idx $i "$@"`
+ commit_id=$(echo $s | cut -d' ' -f1)
+ commit_time=$(echo $s | sed -e "s/^$commit_id //g")
+ printf '%s{"short":false,"id":"%s","author":"%s","date":"%s","message":"%s","diffstat":{},"changes":{}}' \
+ "$comma" "$commit_id" "$GOT_AUTHOR" "$commit_time" "make changes\n"
+ comma=","
+ done >> $testroot/stdout.expected
+ echo "]}" >> $testroot/stdout.expected
+
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$ret"
+}
+
+test_many_commits_summarized() {
+ local testroot=`test_init many_commits_summarized 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got checkout -q $testroot/repo-clone $testroot/wt >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ for i in `seq 1 51`; do
+ echo "alpha $i" > $testroot/wt/alpha
+ (cd $testroot/wt && got commit -m 'make changes' > /dev/null)
+ local commit_id=`git_show_head $testroot/repo-clone`
+ local short_commit_id=`trim_obj_id 33 $commit_id`
+ local author_time=`git_show_author_time $testroot/repo-clone`
+ d=`date -u -r $author_time +"%G-%m-%d"`
+ set -- "$@" "$short_commit_id $d"
+ done
+
+ timeout 5 ./http-server -p "$GOTD_TEST_HTTP_PORT" \
+ > $testroot/stdout &
+
+ got send -b main -q -r $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ wait %1 # wait for the http "server"
+
+ cat <<-EOF > $testroot/stdout.expected
+ POST / HTTP/1.1
+ Host: localhost:${GOTD_TEST_HTTP_PORT}
+ Content-Type: application/json
+ Content-Length: 4864
+ User-Agent: got-notify-http/${GOT_VERSION_STR}
+ Connection: close
+
+ EOF
+
+ printf '{"notifications":[' >> $testroot/stdout.expected
+ comma=""
+ for i in `seq 1 51`; do
+ s=`pop_idx $i "$@"`
+ commit_id=$(echo $s | cut -d' ' -f1)
+ commit_time=$(echo $s | sed -e "s/^$commit_id //g")
+ printf '%s{"short":true,"id":"%s","author":"%s","date":"%s","message":"%s"}' \
+ "$comma" "$commit_id" "$GOT_AUTHOR_8" \
+ "$commit_time" "make changes"
+ comma=","
+ done >> $testroot/stdout.expected
+ echo "]}" >> $testroot/stdout.expected
+
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$ret"
+}
+
+test_parseargs "$@"
+run_test test_file_changed
+run_test test_many_commits_not_summarized
+run_test test_many_commits_summarized