commit ba97b2d7ec97e54a025a39e04c0fd59fdd54a57b from: Stefan Sperling date: Wed Mar 20 08:42:10 2024 UTC add initial support for commit notifications to gotd(8) At present only email notifications are implemented. Code for HTTP notifications is not yet finished, hence HTTP-related documentation remains hidden for now. This adds a new 'notify' process which has an "exec" pledge. It runs helper programs which implement the notification transport layer, such as got-notify-email which speaks SMTP. This design avoids having to link all of gotd with network libraries and related crypto libraries. Notification content is generated by the 'repo_write' process. Commit log messages and diffstats are written to a file which the 'notify' process will pass on to its helpers on stdin. The default output looks similar to 'got log -d'. If too many new commits are present the output looks similar to 'got log -s' instead. Tags always look like 'got tag -l'. The session process coordinates generation of notifications. It maintains a notification queue which holds one notification per updated reference, and passes notification requests from this queue to the 'repo_write' process for notification content creation and then to the 'notify' process for notification delivery. Only one notification can be in flight at a time to avoid file descriptor starvation if many references get updated in a single client session. ok op@ commit - 0f8a0e25ffdf6a715f2a7528ef38eefaa474790a commit + ba97b2d7ec97e54a025a39e04c0fd59fdd54a57b blob - 28645f07dc93480582fce4b5f7dea5813b028a23 blob + 326421d114b5877a893a1f63613d87e64dde3559 --- gitwrapper/gitwrapper.c +++ gitwrapper/gitwrapper.c @@ -172,7 +172,7 @@ main(int argc, char *argv[]) goto done; } - repo = gotd_find_repo_by_name(repo_name, &gotd); + repo = gotd_find_repo_by_name(repo_name, &gotd.repos); /* * Invoke our custom Git server if the repository was found blob - 877e4d8fa52533fe457b51eefc3aed259e36e6a5 blob + 72ddf39763d42262c40de7298eb23144cf2d1703 --- gotd/Makefile +++ gotd/Makefile @@ -1,5 +1,7 @@ .PATH:${.CURDIR}/../lib +SUBDIR = libexec + .include "../got-version.mk" .if ${GOT_RELEASE} == "Yes" @@ -15,7 +17,11 @@ SRCS= gotd.c auth.c repo_read.c repo_write.c log.c pr object_open_io.c object_parse.c opentemp.c pack.c path.c \ read_gitconfig.c read_gotconfig.c reference.c repository.c \ hash.c sigs.c pack_create_io.c pollfd.c reference_parse.c \ - repo_imsg.c pack_index.c session.c object_qid.c + repo_imsg.c pack_index.c session.c object_qid.c notify.c \ + commit_graph.c diffreg.c diff.c \ + diff_main.c diff_atomize_text.c diff_myers.c diff_output.c \ + diff_output_plain.c diff_output_unidiff.c \ + diff_output_edscript.c diff_patience.c CLEANFILES = parse.h blob - b46b5573bc4b7b42b5b311fec8206262a1111da3 blob + 82cbd62438015fab107bd4aa25d20bd02e800127 --- gotd/gotd.c +++ gotd/gotd.c @@ -48,6 +48,7 @@ #include "got_repository.h" #include "got_object.h" #include "got_reference.h" +#include "got_diff.h" #include "got_lib_delta.h" #include "got_lib_object.h" @@ -64,6 +65,7 @@ #include "session.h" #include "repo_read.h" #include "repo_write.h" +#include "notify.h" #ifndef nitems #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) @@ -300,6 +302,9 @@ proc_done(struct gotd_child_proc *proc) disconnect(client); } + if (proc == gotd.notify_proc) + gotd.notify_proc = NULL; + evtimer_del(&proc->tmo); if (proc->iev.ibuf.fd != -1) { @@ -531,7 +536,7 @@ start_client_authentication(struct gotd_client *client err = ensure_client_is_not_writing(client); if (err) return err; - repo = gotd_find_repo_by_name(ireq.repo_name, &gotd); + repo = gotd_find_repo_by_name(ireq.repo_name, &gotd.repos); if (repo == NULL) return got_error(GOT_ERR_NOT_GIT_REPO); err = start_auth_child(client, GOTD_AUTH_READ, repo, @@ -543,7 +548,7 @@ start_client_authentication(struct gotd_client *client err = ensure_client_is_not_reading(client); if (err) return err; - repo = gotd_find_repo_by_name(ireq.repo_name, &gotd); + repo = gotd_find_repo_by_name(ireq.repo_name, &gotd.repos); if (repo == NULL) return got_error(GOT_ERR_NOT_GIT_REPO); err = start_auth_child(client, @@ -735,7 +740,8 @@ static const char *gotd_proc_names[PROC_MAX] = { "session_write", "repo_read", "repo_write", - "gitwrapper" + "gitwrapper", + "notify" }; static void @@ -1116,6 +1122,72 @@ done: } static void +gotd_dispatch_notifier(int fd, short event, void *arg) +{ + struct gotd_imsgev *iev = arg; + struct imsgbuf *ibuf = &iev->ibuf; + struct gotd_child_proc *proc = gotd.notify_proc; + ssize_t n; + int shut = 0; + struct imsg imsg; + + if (proc->iev.ibuf.fd != fd) + fatalx("%s: unexpected fd %d", __func__, fd); + + if (event & EV_READ) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatal("imsg_read error"); + if (n == 0) { + /* Connection closed. */ + shut = 1; + goto done; + } + } + + if (event & EV_WRITE) { + n = msgbuf_write(&ibuf->w); + if (n == -1 && errno != EAGAIN) + fatal("msgbuf_write"); + if (n == 0) { + /* Connection closed. */ + shut = 1; + goto done; + } + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("%s: imsg_get error", __func__); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + default: + log_debug("unexpected imsg %d", imsg.hdr.type); + break; + } + + imsg_free(&imsg); + } +done: + if (!shut) { + gotd_imsg_event_add(iev); + } else { + /* This pipe is dead. Remove its event handler */ + event_del(&iev->ev); + + /* + * Do not exit all of gotd if the notification handler dies. + * We can continue operating without notifications until an + * operator intervenes. + */ + log_warnx("notify child process (pid %d) closed its imsg pipe " + "unexpectedly", proc->pid); + proc_done(proc); + } +} + +static void gotd_dispatch_auth_child(int fd, short event, void *arg) { const struct got_error *err = NULL; @@ -1216,7 +1288,7 @@ gotd_dispatch_auth_child(int fd, short event, void *ar goto done; } - repo = gotd_find_repo_by_name(client->auth->repo_name, &gotd); + repo = gotd_find_repo_by_name(client->auth->repo_name, &gotd.repos); if (repo == NULL) { err = got_error(GOT_ERR_NOT_GIT_REPO); goto done; @@ -1251,6 +1323,7 @@ connect_session(struct gotd_client *client) const struct got_error *err = NULL; struct gotd_imsg_connect iconnect; int s; + struct ibuf *wbuf; memset(&iconnect, 0, sizeof(iconnect)); @@ -1261,14 +1334,28 @@ connect_session(struct gotd_client *client) iconnect.client_id = client->id; iconnect.euid = client->euid; iconnect.egid = client->egid; + iconnect.username_len = strlen(client->username); - if (gotd_imsg_compose_event(&client->session->iev, GOTD_IMSG_CONNECT, - PROC_GOTD, s, &iconnect, sizeof(iconnect)) == -1) { + wbuf = imsg_create(&client->session->iev.ibuf, GOTD_IMSG_CONNECT, + PROC_GOTD, gotd.pid, sizeof(iconnect) + iconnect.username_len); + if (wbuf == NULL) { err = got_error_from_errno("imsg compose CONNECT"); close(s); return err; + } + if (imsg_add(wbuf, &iconnect, sizeof(iconnect)) == -1) { + close(s); + return got_error_from_errno("imsg_add CONNECT"); + } + if (imsg_add(wbuf, client->username, iconnect.username_len) == -1) { + close(s); + return got_error_from_errno("imsg_add CONNECT"); } + ibuf_fd_set(wbuf, s); + imsg_close(&client->session->iev.ibuf, wbuf); + gotd_imsg_event_add(&client->session->iev); + /* * We are no longer interested in messages from this client. * Further client requests will be handled by the session process. @@ -1368,7 +1455,7 @@ gotd_dispatch_client_session(int fd, short event, void struct gotd_repo *repo; const char *name = client->session->repo_name; - repo = gotd_find_repo_by_name(name, &gotd); + repo = gotd_find_repo_by_name(name, &gotd.repos); if (repo != NULL) { enum gotd_procid proc_type; @@ -1405,7 +1492,41 @@ done: /* This pipe is dead. Remove its event handler */ event_del(&iev->ev); disconnect(client); + } +} + +static const struct got_error * +connect_notifier_and_session(struct gotd_client *client) +{ + const struct got_error *err = NULL; + struct gotd_imsgev *session_iev = &client->session->iev; + int pipe[2]; + + if (gotd.notify_proc == NULL) + return NULL; + + if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, + PF_UNSPEC, pipe) == -1) + return got_error_from_errno("socketpair"); + + /* Pass notifier pipe to session . */ + if (gotd_imsg_compose_event(session_iev, GOTD_IMSG_CONNECT_NOTIFIER, + PROC_GOTD, pipe[0], NULL, 0) == -1) { + err = got_error_from_errno("imsg compose CONNECT_NOTIFIER"); + close(pipe[0]); + close(pipe[1]); + return err; + } + + /* Pass session pipe to notifier. */ + if (gotd_imsg_compose_event(&gotd.notify_proc->iev, + GOTD_IMSG_CONNECT_SESSION, PROC_GOTD, pipe[1], NULL, 0) == -1) { + err = got_error_from_errno("imsg compose CONNECT_SESSION"); + close(pipe[1]); + return err; } + + return NULL; } static void @@ -1471,6 +1592,9 @@ gotd_dispatch_repo_child(int fd, short event, void *ar err = connect_session(client); if (err) break; + err = connect_notifier_and_session(client); + if (err) + break; err = connect_repo_child(client, proc); break; default: @@ -1550,6 +1674,9 @@ start_child(enum gotd_procid proc_id, const char *repo case PROC_REPO_WRITE: argv[argc++] = (char *)"-W"; break; + case PROC_NOTIFY: + argv[argc++] = (char *)"-N"; + break; default: fatalx("invalid process id %d", proc_id); } @@ -1601,6 +1728,37 @@ start_listener(char *argv0, const char *confpath, int proc->iev.handler_arg = NULL; gotd.listen_proc = proc; +} + +static void +start_notifier(char *argv0, const char *confpath, int daemonize, int verbosity) +{ + struct gotd_child_proc *proc; + + proc = calloc(1, sizeof(*proc)); + if (proc == NULL) + fatal("calloc"); + + TAILQ_INSERT_HEAD(&procs, proc, entry); + + /* proc->tmo is initialized in main() after event_init() */ + + proc->type = PROC_NOTIFY; + + if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, + PF_UNSPEC, proc->pipe) == -1) + fatal("socketpair"); + + proc->pid = start_child(proc->type, NULL, argv0, confpath, + proc->pipe[1], daemonize, verbosity); + imsg_init(&proc->iev.ibuf, proc->pipe[0]); + proc->iev.handler = gotd_dispatch_notifier; + proc->iev.events = EV_READ; + proc->iev.handler_arg = NULL; + event_set(&proc->iev.ev, proc->iev.ibuf.fd, EV_READ, + gotd_dispatch_notifier, &proc->iev); + + gotd.notify_proc = proc; } static const struct got_error * @@ -1819,6 +1977,25 @@ set_max_datasize(void) rl.rlim_cur = rl.rlim_max; setrlimit(RLIMIT_DATA, &rl); +} + +static void +unveil_notification_helpers(void) +{ + const char *helpers[] = { + GOTD_PATH_PROG_NOTIFY_EMAIL, + GOTD_PATH_PROG_NOTIFY_HTTP, + }; + size_t i; + + for (i = 0; i < nitems(helpers); i++) { + if (unveil(helpers[i], "x") == 0) + continue; + fatal("unveil %s", helpers[i]); + } + + if (unveil(NULL, NULL) == -1) + fatal("unveil"); } int @@ -1835,12 +2012,16 @@ main(int argc, char **argv) struct event evsigint, evsigterm, evsighup, evsigusr1, evsigchld; int *pack_fds = NULL, *temp_fds = NULL; struct gotd_repo *repo = NULL; + char *default_sender = NULL; + char hostname[HOST_NAME_MAX + 1]; + FILE *diff_f1 = NULL, *diff_f2 = NULL; + int diff_fd1 = -1, diff_fd2 = -1; TAILQ_INIT(&procs); log_init(1, LOG_DAEMON); /* Log to stderr until daemonized. */ - while ((ch = getopt(argc, argv, "Adf:LnP:RsSvW")) != -1) { + while ((ch = getopt(argc, argv, "Adf:LnNP:RsSvW")) != -1) { switch (ch) { case 'A': proc_id = PROC_AUTH; @@ -1857,6 +2038,9 @@ main(int argc, char **argv) case 'n': noaction = 1; break; + case 'N': + proc_id = PROC_NOTIFY; + break; case 'P': repo_path = realpath(optarg, NULL); if (repo_path == NULL) @@ -1926,6 +2110,7 @@ main(int argc, char **argv) fatal("daemon"); gotd.pid = getpid(); start_listener(argv0, confpath, daemonize, verbosity); + start_notifier(argv0, confpath, daemonize, verbosity); } else if (proc_id == PROC_LISTEN) { snprintf(title, sizeof(title), "%s", gotd_proc_names[proc_id]); if (verbosity) { @@ -1954,6 +2139,13 @@ main(int argc, char **argv) fatalx("repository path not specified"); snprintf(title, sizeof(title), "%s %s", gotd_proc_names[proc_id], repo_path); + } else if (proc_id == PROC_NOTIFY) { + snprintf(title, sizeof(title), "%s", gotd_proc_names[proc_id]); + if (gethostname(hostname, sizeof(hostname)) == -1) + fatal("gethostname"); + if (asprintf(&default_sender, "%s@%s", + pw->pw_name, hostname) == -1) + fatal("asprintf"); } else fatal("invalid process id %d", proc_id); @@ -2020,10 +2212,14 @@ main(int argc, char **argv) #endif if (proc_id == PROC_SESSION_READ) apply_unveil_repo_readonly(repo_path, 1); - else + else { apply_unveil_repo_readwrite(repo_path); + repo = gotd_find_repo_by_path(repo_path, &gotd); + if (repo == NULL) + fatalx("no repository for path %s", repo_path); + } session_main(title, repo_path, pack_fds, temp_fds, - &gotd.request_timeout, proc_id); + &gotd.request_timeout, repo, proc_id); /* NOTREACHED */ break; case PROC_REPO_READ: @@ -2038,6 +2234,19 @@ main(int argc, char **argv) exit(0); case PROC_REPO_WRITE: set_max_datasize(); + + diff_f1 = got_opentemp(); + if (diff_f1 == NULL) + fatal("got_opentemp"); + diff_f2 = got_opentemp(); + if (diff_f2 == NULL) + fatal("got_opentemp"); + diff_fd1 = got_opentempfd(); + if (diff_fd1 == -1) + fatal("got_opentempfd"); + diff_fd2 = got_opentempfd(); + if (diff_fd2 == -1) + fatal("got_opentempfd"); #ifndef PROFILE if (pledge("stdio rpath recvfd unveil", NULL) == -1) err(1, "pledge"); @@ -2047,11 +2256,25 @@ main(int argc, char **argv) if (repo == NULL) fatalx("no repository for path %s", repo_path); repo_write_main(title, repo_path, pack_fds, temp_fds, + diff_f1, diff_f2, diff_fd1, diff_fd2, &repo->protected_tag_namespaces, &repo->protected_branch_namespaces, &repo->protected_branches); /* NOTREACHED */ exit(0); + case PROC_NOTIFY: +#ifndef PROFILE + if (pledge("stdio proc exec recvfd unveil", NULL) == -1) + err(1, "pledge"); +#endif + /* + * Limit "exec" promise to notification helpers via unveil(2). + */ + unveil_notification_helpers(); + + notify_main(title, &gotd.repos, default_sender); + /* NOTREACHED */ + exit(0); default: fatal("invalid process id %d", proc_id); } @@ -2061,6 +2284,10 @@ main(int argc, char **argv) evtimer_set(&gotd.listen_proc->tmo, kill_proc_timeout, gotd.listen_proc); + if (gotd.notify_proc) { + evtimer_set(&gotd.notify_proc->tmo, kill_proc_timeout, + gotd.notify_proc); + } apply_unveil_selfexec(); @@ -2078,10 +2305,13 @@ main(int argc, char **argv) signal_add(&evsigchld, NULL); gotd_imsg_event_add(&gotd.listen_proc->iev); + if (gotd.notify_proc) + gotd_imsg_event_add(&gotd.notify_proc->iev); event_dispatch(); free(repo_path); + free(default_sender); gotd_shutdown(); return 0; blob - 09928aa29395cb1acfaff6303c26cda1adfaf34a blob + 45a21b3bd385d22ec6ae514d311b7bd4a6753183 --- gotd/gotd.conf.5 +++ gotd/gotd.conf.5 @@ -237,6 +237,132 @@ do not need to be listed in .Nm . These namespaces are always protected and even attempts to create new references in these namespaces will always be denied. +.It Ic notify Brq Ar ... +The +.Ic notify +directive enables notifications about new commits or tags +added to the repository. +.Pp +Notifications via email require an SMTP daemon which accepts mail +for forwarding without requiring client authentication or encryption. +On +.Ox +the +.Xr smtpd 8 +daemon can be used for this purpose. +The default content of email notifications looks similar to the output of the +.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 +The +.Ic notify +directive expects parameters which must be enclosed in curly braces. +The available parameters are as follows: +.Pp +.Bl -tag -width Ds +.It Ic branch Ar name +Send notifications about commits to the named branch. +The +.Ar name +will be looked up in the +.Dq refs/heads/ +reference namespace. +This directive may be specified multiple times to build a list of +branches to send notifications for. +If neither a +.Ic branch +nor a +.Ic reference namespace +are specified then changes to any reference will trigger notifications. +.It Ic reference Ic namespace Ar namespace +Send notifications about commits or tags within a reference namespace. +This directive may be specified multiple times to build a list of +namespaces to send notifications for. +If neither a +.Ic branch +nor a +.Ic reference namespace +are specified then changes to any reference will trigger notifications. +.It Ic email Oo Ic from Ar sender Oc Ic to Ar recipient Oo Ic reply to Ar responder Oc Oo Ic relay Ar hostname Oo Ic port Ar port Oc Oc +Send notifications via email to the specified +.Ar recipient . +This directive may be specified multiple times to build a list of +recipients to send notifications to. +.Pp +The +.Ar recipient +must be an email addresses that accepts mail. +The +.Ar sender +will be used as the From address. +If not specified, the sender defaults to an email address composed of the user +account running +.Xr gotd 8 +and the local hostname. +.Pp +If a +.Ar responder +is specified via the +.Ic reply to +directive, the +.Ar responder +will be used as the Reply-to address. +Setting the Reply-to header can be useful if replies should go to a +mailing list instead of the +.Ar sender , +for example. +.Pp +By default, mail will be sent to the SMTP server listening on the loopback +address 127.0.0.1 on port 25. +The +.Ic relay +and +.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 .El .Sh FILES .Bl -tag -width Ds -compact @@ -276,6 +402,13 @@ repository "openbsd/ports" { branch "main" tag namespace "refs/tags/" } + + notify { + branch "main" + reference namespace "refs/tags/" + email to openbsd-ports-changes@example.com +.\" url https://example.com/notify/ user "flan_announcer" password "secret" + } } # Use a larger request timeout value: blob - acb40dee8cd351b48669c3c3247c42ed5f44501b blob + d6d5b413a46c4062f8b7a4a635cd9ffea6d6c49f --- gotd/gotd.h +++ gotd/gotd.h @@ -21,6 +21,23 @@ #define GOTD_CONF_PATH "/etc/gotd.conf" #define GOTD_EMPTY_PATH "/var/empty" +#ifndef GOT_LIBEXECDIR +#define GOT_LIBEXECDIR /usr/libexec +#endif + +#define GOTD_STRINGIFY(x) #x +#define GOTD_STRINGVAL(x) GOTD_STRINGIFY(x) + +#define GOTD_PROG_NOTIFY_EMAIL got-notify-email +#define GOTD_PROG_NOTIFY_HTTP got-notify-http + +#define GOTD_PATH_PROG_NOTIFY_EMAIL \ + GOTD_STRINGVAL(GOT_LIBEXECDIR) "/" \ + GOTD_STRINGVAL(GOTD_PROG_NOTIFY_EMAIL) +#define GOTD_PATH_PROG_NOTIFY_HTTP \ + GOTD_STRINGVAL(GOT_LIBEXECDIR) "/" \ + GOTD_STRINGVAL(GOTD_PROG_NOTIFY_HTTP) + #define GOTD_MAXCLIENTS 1024 #define GOTD_MAX_CONN_PER_UID 4 #define GOTD_FD_RESERVE 5 @@ -41,6 +58,7 @@ enum gotd_procid { PROC_REPO_READ, PROC_REPO_WRITE, PROC_GITWRAPPER, + PROC_NOTIFY, PROC_MAX, }; @@ -70,6 +88,32 @@ struct gotd_access_rule { }; STAILQ_HEAD(gotd_access_rule_list, gotd_access_rule); +enum gotd_notification_target_type { + GOTD_NOTIFICATION_VIA_EMAIL, + GOTD_NOTIFICATION_VIA_HTTP +}; + +struct gotd_notification_target { + STAILQ_ENTRY(gotd_notification_target) entry; + + enum gotd_notification_target_type type; + union { + struct { + char *sender; + char *recipient; + char *responder; + char *hostname; + char *port; + } email; + struct { + char *url; + char *user; + char *password; + } http; + } conf; +}; +STAILQ_HEAD(gotd_notification_targets, gotd_notification_target); + struct gotd_repo { TAILQ_ENTRY(gotd_repo) entry; @@ -80,6 +124,10 @@ struct gotd_repo { struct got_pathlist_head protected_tag_namespaces; struct got_pathlist_head protected_branch_namespaces; struct got_pathlist_head protected_branches; + + struct got_pathlist_head notification_refs; + struct got_pathlist_head notification_ref_namespaces; + struct gotd_notification_targets notification_targets; }; TAILQ_HEAD(gotd_repolist, gotd_repo); @@ -93,6 +141,7 @@ enum gotd_session_state { GOTD_STATE_EXPECT_PACKFILE, GOTD_STATE_EXPECT_DONE, GOTD_STATE_DONE, + GOTD_STATE_NOTIFY, }; struct gotd_client_capability { @@ -120,6 +169,8 @@ struct gotd { struct gotd_repolist repos; int nrepos; struct gotd_child_proc *listen_proc; + struct gotd_child_proc *notify_proc; + int notifications_enabled; struct timeval request_timeout; struct timeval auth_timeout; struct gotd_uid_connection_limit *connection_limits; @@ -193,6 +244,12 @@ enum gotd_imsg_type { /* Auth child process. */ GOTD_IMSG_AUTHENTICATE, GOTD_IMSG_ACCESS_GRANTED, + + /* Notify child process. */ + GOTD_IMSG_CONNECT_NOTIFIER, + GOTD_IMSG_CONNECT_SESSION, + GOTD_IMSG_NOTIFY, + GOTD_IMSG_NOTIFICATION_SENT }; /* Structure for GOTD_IMSG_ERROR. */ @@ -425,6 +482,9 @@ struct gotd_imsg_connect { uint32_t client_id; uid_t euid; gid_t egid; + size_t username_len; + + /* Followed by username_len data bytes. */ }; /* Structure for GOTD_IMSG_CONNECT_REPO_CHILD. */ @@ -440,15 +500,38 @@ struct gotd_imsg_auth { uid_t euid; gid_t egid; int required_auth; + uint32_t client_id; +}; + +/* Structures for GOTD_IMSG_NOTIFY. */ +enum gotd_notification_action { + GOTD_NOTIF_ACTION_CREATED, + GOTD_NOTIF_ACTION_REMOVED, + GOTD_NOTIF_ACTION_CHANGED +}; +/* IMSG_NOTIFY session <-> repo_write */ +struct gotd_imsg_notification_content { uint32_t client_id; + enum gotd_notification_action action; + uint8_t old_id[SHA1_DIGEST_LENGTH]; + uint8_t new_id[SHA1_DIGEST_LENGTH]; + size_t refname_len; + /* Followed by refname_len data bytes. */ }; +/* IMSG_NOTIFY session -> notify*/ +struct gotd_imsg_notify { + char repo_name[NAME_MAX]; + char subject_line[64]; +}; int parse_config(const char *, enum gotd_procid, struct gotd *); -struct gotd_repo *gotd_find_repo_by_name(const char *, struct gotd *); +struct gotd_repo *gotd_find_repo_by_name(const char *, struct gotd_repolist *); struct gotd_repo *gotd_find_repo_by_path(const char *, struct gotd *); struct gotd_uid_connection_limit *gotd_find_uid_connection_limit( struct gotd_uid_connection_limit *limits, size_t nlimits, uid_t uid); int gotd_parseuid(const char *s, uid_t *uid); +const struct got_error *gotd_parse_url(char **, char **, char **, + char **, const char *); /* imsg.c */ const struct got_error *gotd_imsg_flush(struct imsgbuf *); blob - /dev/null blob + 59c09c35e0b26fb30a138fc1dd52d8f994826c32 (mode 644) --- /dev/null +++ gotd/libexec/Makefile @@ -0,0 +1,3 @@ +SUBDIR = got-notify-email + +.include blob - /dev/null blob + 1bc739e7aa3fede7385d957470cee88dd2460099 (mode 644) --- /dev/null +++ gotd/libexec/Makefile.inc @@ -0,0 +1,7 @@ +.include "../../Makefile.inc" + +realinstall: + ${INSTALL} ${INSTALL_COPY} -o ${BINOWN} -g ${BINGRP} \ + -m ${BINMODE} ${PROG} ${LIBEXECDIR}/${PROG} + +NOMAN = Yes blob - /dev/null blob + bc6f702e1ea25327f3545179d493446d81da8948 (mode 644) --- /dev/null +++ gotd/libexec/got-notify-email/Makefile @@ -0,0 +1,20 @@ +.PATH:${.CURDIR}/../.. +.PATH:${.CURDIR}/../../../lib + +.include "../../../got-version.mk" + +PROG= got-notify-email +SRCS= got-notify-email.c pollfd.c error.c hash.c + + +CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib + +.if defined(PROFILE) +LDADD = -lutil_p -lz_p -lm_p +.else +LDADD = -lutil -lz -lm +.endif + +DPADD = ${LIBZ} ${LIBUTIL} + +.include blob - /dev/null blob + 44ccd53effeef92d860796c2bea93b56f5aa539a (mode 644) --- /dev/null +++ gotd/libexec/got-notify-email/got-notify-email.c @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2024 Stefan Sperling + * + * 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "got_error.h" + +#include "got_lib_poll.h" + +int smtp_timeout = 60; /* in seconds */ + +__dead static void +usage(void) +{ + fprintf(stderr, "usage: %s [-f sender ] [-r responder] " + "[-s subject] [-h hostname] [-p port] recipient\n", getprogname()); + exit(1); +} + +static char * +set_default_fromaddr(void) +{ + struct passwd *pw = NULL; + char *s; + char hostname[255]; + + pw = getpwuid(getuid()); + if (pw == NULL) { + errx(1, "my UID %d was not found in password database", + getuid()); + } + + if (gethostname(hostname, sizeof(hostname)) == -1) + err(1, "gethostname"); + + if (asprintf(&s, "%s@%s", pw->pw_name, hostname) == -1) + err(1, "asprintf"); + + return s; +} + +static int +read_smtp_code(int s, const char *code) +{ + const struct got_error *error; + char buf[4]; + size_t n; + + error = got_poll_read_full_timeout(s, &n, buf, 3, 3, smtp_timeout); + if (error) + errx(1, "read: %s", error->msg); + if (strncmp(buf, code, 3) != 0) { + buf[3] = '\0'; + warnx("unexpected SMTP message code: %s", buf); + return -1; + } + + return 0; +} + +static int +skip_to_crlf(int s) +{ + const struct got_error *error; + char buf[1]; + size_t len; + + for (;;) { + error = got_poll_read_full_timeout(s, &len, buf, 1, 1, + smtp_timeout); + if (error) + errx(1, "read: %s", error->msg); + if (buf[0] == '\r') { + error = got_poll_read_full(s, &len, buf, 1, 1); + if (error) + errx(1, "read: %s", error->msg); + if (buf[0] == '\n') + return 0; + } + } + + return -1; +} + +static int +send_smtp_msg(int s, const char *fmt, ...) +{ + const struct got_error *error; + char buf[512]; + int len; + va_list ap; + + va_start(ap, fmt); + len = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + if (len < 0) { + warn("vsnprintf"); + return -1; + } + if (len >= sizeof(buf)) { + warnx("%s: buffer too small for message '%s...'", + __func__, buf); + return -1; + } + + error = got_poll_write_full(s, buf, len); + if (error) { + warnx("write: %s", error->msg); + return -1; + } + + return 0; +} + +static char * +get_datestr(time_t *time, char *datebuf) +{ + struct tm mytm, *tm; + char *p, *s; + + tm = gmtime_r(time, &mytm); + if (tm == NULL) + return NULL; + s = asctime_r(tm, datebuf); + if (s == NULL) + return NULL; + p = strchr(s, '\n'); + if (p) + *p = '\0'; + return s; +} + +static void +send_email(const char *myfromaddr, const char *fromaddr, + const char *recipient, const char *replytoaddr, + const char *subject, const char *hostname, const char *port) +{ + const struct got_error *error; + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + struct addrinfo hints, *res = NULL; + int s = -1, ret; + time_t now; + char datebuf[26]; + char *datestr; + + now = time(NULL); + datestr = get_datestr(&now, datebuf); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + ret = getaddrinfo(hostname, port, &hints, &res); + if (ret) + errx(1, "getaddrinfo: %s", gai_strerror(ret)); + + s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (s == -1) + err(1, "socket"); + + if (connect(s, res->ai_addr, res->ai_addrlen) == -1) + err(1, "connect %s:%s", hostname, port); + + if (read_smtp_code(s, "220")) + errx(1, "unexpected SMTP greeting received"); + if (skip_to_crlf(s)) + errx(1, "invalid SMTP message received"); + + if (send_smtp_msg(s, "HELO localhost\r\n")) + errx(1, "could not send HELO"); + if (read_smtp_code(s, "250")) + errx(1, "unexpected SMTP response received"); + if (skip_to_crlf(s)) + errx(1, "invalid SMTP message received"); + + if (send_smtp_msg(s, "MAIL FROM:<%s>\r\n", myfromaddr)) + errx(1, "could not send MAIL FROM"); + if (read_smtp_code(s, "250")) + errx(1, "unexpected SMTP response received"); + if (skip_to_crlf(s)) + errx(1, "invalid SMTP message received"); + + if (send_smtp_msg(s, "RCPT TO:<%s>\r\n", recipient)) + errx(1, "could not send MAIL FROM"); + if (read_smtp_code(s, "250")) + errx(1, "unexpected SMTP response received"); + if (skip_to_crlf(s)) + errx(1, "invalid SMTP message received"); + + if (send_smtp_msg(s, "DATA\r\n")) + errx(1, "could not send MAIL FROM"); + if (read_smtp_code(s, "354")) + errx(1, "unexpected SMTP response received"); + if (skip_to_crlf(s)) + errx(1, "invalid SMTP message received"); + + if (send_smtp_msg(s, "From: %s\r\n", fromaddr)) + errx(1, "could not send From header"); + if (send_smtp_msg(s, "To: %s\r\n", recipient)) + errx(1, "could not send To header"); + if (replytoaddr) { + if (send_smtp_msg(s, "Reply-To: %s\r\n", replytoaddr)) + errx(1, "could not send Reply-To header"); + } + if (send_smtp_msg(s, "Date: %s +0000 (UTC)\r\n", datestr)) + errx(1, "could not send Date header"); + + if (send_smtp_msg(s, "Subject: %s\r\n", subject)) + errx(1, "could not send Subject header"); + + if (send_smtp_msg(s, "\r\n")) + errx(1, "could not send body delimiter"); + + while ((linelen = getline(&line, &linesize, stdin)) != -1) { + if (line[0] == '.') { /* dot stuffing */ + error = got_poll_write_full(s, ".", 1); + if (error) + errx(1, "write: %s", error->msg); + } + error = got_poll_write_full(s, line, linelen); + if (error) + errx(1, "write: %s", error->msg); + } + + if (send_smtp_msg(s, "\r\n.\r\n")) + errx(1, "could not send data terminator"); + if (read_smtp_code(s, "250")) + errx(1, "unexpected SMTP response received"); + if (skip_to_crlf(s)) + errx(1, "invalid SMTP message received"); + + if (send_smtp_msg(s, "QUIT\r\n")) + errx(1, "could not send QUIT"); + + if (read_smtp_code(s, "221")) + errx(1, "unexpected SMTP response received"); + if (skip_to_crlf(s)) + errx(1, "invalid SMTP message received"); + + close(s); + free(line); + if (res) + freeaddrinfo(res); +} + +int +main(int argc, char *argv[]) +{ + char *default_fromaddr = NULL; + const char *fromaddr = NULL, *recipient = NULL, *replytoaddr = NULL; + const char *subject = "gotd notification"; + const char *hostname = "127.0.0.1"; + const char *port = "25"; + const char *errstr; + char *timeoutstr; + int ch; + + while ((ch = getopt(argc, argv, "f:r:s:h:p:")) != -1) { + switch (ch) { + case 'h': + hostname = optarg; + break; + case 'f': + fromaddr = optarg; + break; + case 'p': + port = optarg; + break; + case 'r': + replytoaddr = optarg; + break; + case 's': + subject = optarg; + break; + default: + usage(); + /* NOTREACHED */ + break; + } + } + + argc -= optind; + argv += optind; + + if (argc != 1) + usage(); + + /* used by the regression test suite */ + timeoutstr = getenv("GOT_NOTIFY_EMAIL_TIMEOUT"); + if (timeoutstr) { + smtp_timeout = strtonum(timeoutstr, 0, 600, &errstr); + if (errstr != NULL) + errx(1, "timeout in seconds is %s: %s", + errstr, timeoutstr); + } + +#ifndef PROFILE + if (pledge("stdio dns inet getpw", NULL) == -1) + err(1, "pledge"); +#endif + default_fromaddr = set_default_fromaddr(); + +#ifndef PROFILE + if (pledge("stdio dns inet", NULL) == -1) + err(1, "pledge"); +#endif + + recipient = argv[0]; + if (fromaddr == NULL) + fromaddr = default_fromaddr; + + send_email(default_fromaddr, fromaddr, recipient, replytoaddr, + subject, hostname, port); + + free(default_fromaddr); + return 0; +} blob - 58db764d105aff68e65237083482558bcf17fb47 blob + 4792873a2f7cc5b8b83fde7afc7b5a1a7f269d58 --- gotd/parse.y +++ gotd/parse.y @@ -73,6 +73,7 @@ int lookup(char *); int lgetc(int); int lungetc(int); int findeol(void); +static char *port_sprintf(int); TAILQ_HEAD(symhead, sym) symhead = TAILQ_HEAD_INITIALIZER(symhead); struct sym { @@ -102,6 +103,14 @@ static int conf_protect_branch_namespace( struct gotd_repo *, char *); static int conf_protect_branch(struct gotd_repo *, char *); +static int conf_notify_branch(struct gotd_repo *, + char *); +static int conf_notify_ref_namespace(struct gotd_repo *, + char *); +static int conf_notify_email(struct gotd_repo *, + char *, char *, char *, char *, char *); +static int conf_notify_http(struct gotd_repo *, + char *, char *, char *); static enum gotd_procid gotd_proc_id; typedef struct { @@ -117,7 +126,8 @@ typedef struct { %token PATH ERROR LISTEN ON USER REPOSITORY PERMIT DENY %token RO RW CONNECTION LIMIT REQUEST TIMEOUT -%token PROTECT NAMESPACE BRANCH TAG +%token PROTECT NAMESPACE BRANCH TAG REFERENCE RELAY PORT +%token NOTIFY EMAIL FROM REPLY TO URL PASSWORD %token STRING %token NUMBER @@ -299,6 +309,320 @@ protectflags : TAG NAMESPACE STRING { } ; +notify : NOTIFY '{' optnl notifyflags_l '}' + | NOTIFY notifyflags + +notifyflags_l : notifyflags optnl notifyflags_l + | notifyflags optnl + ; + +notifyflags : BRANCH STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_branch(new_repo, $2)) { + free($2); + YYERROR; + } + free($2); + } + } + | REFERENCE NAMESPACE STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_ref_namespace(new_repo, $3)) { + free($3); + YYERROR; + } + free($3); + } + } + | EMAIL TO STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, NULL, $3, + NULL, NULL, NULL)) { + free($3); + YYERROR; + } + free($3); + } + } + | EMAIL FROM STRING TO STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, $3, $5, + NULL, NULL, NULL)) { + free($3); + free($5); + YYERROR; + } + free($3); + free($5); + } + } + | EMAIL TO STRING REPLY TO STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, NULL, $3, + $6, NULL, NULL)) { + free($3); + free($6); + YYERROR; + } + free($3); + free($6); + } + } + | EMAIL FROM STRING TO STRING REPLY TO STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, $3, $5, + $8, NULL, NULL)) { + free($3); + free($5); + free($8); + YYERROR; + } + free($3); + free($5); + free($8); + } + } + | EMAIL TO STRING RELAY STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, NULL, $3, + NULL, $5, NULL)) { + free($3); + free($5); + YYERROR; + } + free($3); + free($5); + } + } + | EMAIL FROM STRING TO STRING RELAY STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, $3, $5, + NULL, $7, NULL)) { + free($3); + free($5); + free($7); + YYERROR; + } + free($3); + free($5); + free($7); + } + } + | EMAIL TO STRING REPLY TO STRING RELAY STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, NULL, $3, + $6, $8, NULL)) { + free($3); + free($6); + free($8); + YYERROR; + } + free($3); + free($6); + free($8); + } + } + | EMAIL FROM STRING TO STRING REPLY TO STRING RELAY STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, $3, $5, + $8, $10, NULL)) { + free($3); + free($5); + free($8); + free($10); + YYERROR; + } + free($3); + free($5); + free($8); + free($10); + } + } + | EMAIL TO STRING RELAY STRING PORT STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, NULL, $3, + NULL, $5, $7)) { + free($3); + free($5); + free($7); + YYERROR; + } + free($3); + free($5); + free($7); + } + } + | EMAIL FROM STRING TO STRING RELAY STRING PORT STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, $3, $5, + NULL, $7, $9)) { + free($3); + free($5); + free($7); + free($9); + YYERROR; + } + free($3); + free($5); + free($7); + free($9); + } + } + | EMAIL TO STRING REPLY TO STRING RELAY STRING PORT STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, NULL, $3, + $6, $8, $10)) { + free($3); + free($6); + free($8); + free($10); + YYERROR; + } + free($3); + free($6); + free($8); + free($10); + } + } + | EMAIL FROM STRING TO STRING REPLY TO STRING RELAY STRING PORT STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, $3, $5, + $8, $10, $12)) { + free($3); + free($5); + free($8); + free($10); + free($12); + YYERROR; + } + free($3); + free($5); + free($8); + free($10); + free($12); + } + } + | EMAIL TO STRING RELAY STRING PORT NUMBER { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, NULL, $3, + NULL, $5, port_sprintf($7))) { + free($3); + free($5); + YYERROR; + } + free($3); + free($5); + } + } + | EMAIL FROM STRING TO STRING RELAY STRING PORT NUMBER { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, $3, $5, + NULL, $7, port_sprintf($9))) { + free($3); + free($5); + free($7); + YYERROR; + } + free($3); + free($5); + free($7); + } + } + | EMAIL TO STRING REPLY TO STRING RELAY STRING PORT NUMBER { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, NULL, $3, + $6, $8, port_sprintf($10))) { + free($3); + free($6); + free($8); + YYERROR; + } + free($3); + free($6); + free($8); + } + } + | EMAIL FROM STRING TO STRING REPLY TO STRING RELAY STRING PORT NUMBER { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_email(new_repo, $3, $5, + $8, $10, port_sprintf($12))) { + free($3); + free($5); + free($8); + free($10); + YYERROR; + } + free($3); + free($5); + free($8); + free($10); + } + } + | URL STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_http(new_repo, $2, NULL, + NULL)) { + free($2); + YYERROR; + } + free($2); + } + } + | URL STRING USER STRING PASSWORD STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_NOTIFY) { + if (conf_notify_http(new_repo, $2, $4, $6)) { + free($2); + free($4); + free($6); + YYERROR; + } + free($2); + free($4); + free($6); + } + } + ; + repository : REPOSITORY STRING { struct gotd_repo *repo; @@ -313,7 +637,9 @@ repository : REPOSITORY STRING { if (gotd_proc_id == PROC_GOTD || gotd_proc_id == PROC_AUTH || gotd_proc_id == PROC_REPO_WRITE || - gotd_proc_id == PROC_GITWRAPPER) { + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_GITWRAPPER | + gotd_proc_id == PROC_NOTIFY) { new_repo = conf_new_repo($2); } free($2); @@ -325,7 +651,9 @@ repoopts1 : PATH STRING { if (gotd_proc_id == PROC_GOTD || gotd_proc_id == PROC_AUTH || gotd_proc_id == PROC_REPO_WRITE || - gotd_proc_id == PROC_GITWRAPPER) { + gotd_proc_id == PROC_SESSION_WRITE || + gotd_proc_id == PROC_GITWRAPPER || + gotd_proc_id == PROC_NOTIFY) { if (!got_path_is_absolute($2)) { yyerror("%s: path %s is not absolute", __func__, $2); @@ -385,6 +713,7 @@ repoopts1 : PATH STRING { free($2); } | protect + | notify ; repoopts2 : repoopts2 repoopts1 nl @@ -435,19 +764,29 @@ lookup(char *s) { "branch", BRANCH }, { "connection", CONNECTION }, { "deny", DENY }, + { "email", EMAIL }, + { "from", FROM }, { "limit", LIMIT }, { "listen", LISTEN }, { "namespace", NAMESPACE }, + { "notify", NOTIFY }, { "on", ON }, + { "password", PASSWORD }, { "path", PATH }, { "permit", PERMIT }, + { "port", PORT }, { "protect", PROTECT }, + { "reference", REFERENCE }, + { "relay", RELAY }, + { "reply", REPLY }, { "repository", REPOSITORY }, { "request", REQUEST }, { "ro", RO }, { "rw", RW }, { "tag", TAG }, { "timeout", TIMEOUT }, + { "to", TO }, + { "url", URL }, { "user", USER }, }; const struct keywords *p; @@ -919,6 +1258,10 @@ conf_new_repo(const char *name) TAILQ_INIT(&repo->protected_tag_namespaces); TAILQ_INIT(&repo->protected_branch_namespaces); TAILQ_INIT(&repo->protected_branches); + TAILQ_INIT(&repo->protected_branches); + TAILQ_INIT(&repo->notification_refs); + TAILQ_INIT(&repo->notification_ref_namespaces); + STAILQ_INIT(&repo->notification_targets); if (strlcpy(repo->name, name, sizeof(repo->name)) >= sizeof(repo->name)) @@ -1069,12 +1412,193 @@ conf_protect_branch(struct gotd_repo *repo, char *bran yyerror("got_pathlist_insert: %s", error->msg); else yyerror("duplicate protect branch %s", branchname); + return -1; + } + + return 0; +} + +static int +conf_notify_branch(struct gotd_repo *repo, char *branchname) +{ + const struct got_error *error; + struct got_pathlist_entry *pe; + char *refname; + + if (strncmp(branchname, "refs/heads/", 11) != 0) { + if (asprintf(&refname, "refs/heads/%s", branchname) == -1) { + yyerror("asprintf: %s", strerror(errno)); + return -1; + } + } else { + refname = strdup(branchname); + if (refname == NULL) { + yyerror("strdup: %s", strerror(errno)); + return -1; + } + } + + if (!refname_is_valid(refname)) { + free(refname); return -1; } + error = got_pathlist_insert(&pe, &repo->notification_refs, + refname, NULL); + if (error) { + free(refname); + yyerror("got_pathlist_insert: %s", error->msg); + return -1; + } + if (pe == NULL) + free(refname); + return 0; } +static int +conf_notify_ref_namespace(struct gotd_repo *repo, char *namespace) +{ + const struct got_error *error; + struct got_pathlist_entry *pe; + char *s; + + got_path_strip_trailing_slashes(namespace); + if (!refname_is_valid(namespace)) + return -1; + + if (asprintf(&s, "%s/", namespace) == -1) { + yyerror("asprintf: %s", strerror(errno)); + return -1; + } + + error = got_pathlist_insert(&pe, &repo->notification_ref_namespaces, + s, NULL); + if (error) { + free(s); + yyerror("got_pathlist_insert: %s", error->msg); + return -1; + } + if (pe == NULL) + free(s); + + return 0; +} + +static int +conf_notify_email(struct gotd_repo *repo, char *sender, char *recipient, + char *responder, char *hostname, char *port) +{ + struct gotd_notification_target *target; + + STAILQ_FOREACH(target, &repo->notification_targets, entry) { + if (target->type != GOTD_NOTIFICATION_VIA_EMAIL) + continue; + if (strcmp(target->conf.email.recipient, recipient) == 0) { + yyerror("duplicate email notification for '%s' in " + "repository '%s'", recipient, repo->name); + return -1; + } + } + + target = calloc(1, sizeof(*target)); + if (target == NULL) + fatal("calloc"); + target->type = GOTD_NOTIFICATION_VIA_EMAIL; + if (sender) { + target->conf.email.sender = strdup(sender); + if (target->conf.email.sender == NULL) + fatal("strdup"); + } + target->conf.email.recipient = strdup(recipient); + if (target->conf.email.recipient == NULL) + fatal("strdup"); + if (responder) { + target->conf.email.responder = strdup(responder); + if (target->conf.email.responder == NULL) + fatal("strdup"); + } + if (hostname) { + target->conf.email.hostname = strdup(hostname); + if (target->conf.email.hostname == NULL) + fatal("strdup"); + } + if (port) { + target->conf.email.port = strdup(port); + if (target->conf.email.port == NULL) + fatal("strdup"); + } + + STAILQ_INSERT_TAIL(&repo->notification_targets, target, entry); + return 0; +} + +static int +conf_notify_http(struct gotd_repo *repo, char *url, char *user, char *password) +{ + const struct got_error *error; + struct gotd_notification_target *target; + char *proto, *host, *port, *request_path; + int ret = 0; + + error = gotd_parse_url(&proto, &host, &port, &request_path, url); + if (error) { + yyerror("invalid HTTP notification URL '%s' in " + "repository '%s': %s", url, repo->name, error->msg); + return -1; + } + + 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 (strcmp(proto, "http") == 0 && (user != NULL || password != NULL)) { + log_warnx("%s: WARNING: Using basic authentication over " + "plaintext http:// will leak credentials; https:// is " + "recommended for URL '%s'", getprogname(), url); + } + + STAILQ_FOREACH(target, &repo->notification_targets, entry) { + if (target->type != GOTD_NOTIFICATION_VIA_HTTP) + continue; + if (strcmp(target->conf.http.url, url) == 0) { + yyerror("duplicate notification for URL '%s' in " + "repository '%s'", url, repo->name); + ret = -1; + goto done; + } + } + + target = calloc(1, sizeof(*target)); + 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"); + if (user) { + target->conf.http.user = strdup(user); + if (target->conf.http.user == NULL) + fatal("calloc"); + } + if (password) { + target->conf.http.password = strdup(password); + if (target->conf.http.password == NULL) + fatal("calloc"); + } + + STAILQ_INSERT_TAIL(&repo->notification_targets, target, entry); +done: + free(proto); + free(host); + free(port); + free(request_path); + return ret; +} + int symset(const char *nam, const char *val, int persist) { @@ -1131,12 +1655,12 @@ symget(const char *nam) } struct gotd_repo * -gotd_find_repo_by_name(const char *repo_name, struct gotd *gotd) +gotd_find_repo_by_name(const char *repo_name, struct gotd_repolist *repos) { struct gotd_repo *repo; size_t namelen; - TAILQ_FOREACH(repo, &gotd->repos, entry) { + TAILQ_FOREACH(repo, repos, entry) { namelen = strlen(repo->name); if (strncmp(repo->name, repo_name, namelen) != 0) continue; @@ -1198,3 +1722,100 @@ gotd_parseuid(const char *s, uid_t *uid) return -1; return 0; } + +const struct got_error * +gotd_parse_url(char **proto, char **host, char **port, + char **request_path, const char *url) +{ + const struct got_error *err = NULL; + char *s, *p, *q; + + *proto = *host = *port = *request_path = NULL; + + p = strstr(url, "://"); + if (!p) + return got_error(GOT_ERR_PARSE_URI); + + *proto = strndup(url, p - url); + if (*proto == NULL) { + err = got_error_from_errno("strndup"); + goto done; + } + s = p + 3; + + p = strstr(s, "/"); + if (p == NULL || strlen(p) == 1) { + err = got_error(GOT_ERR_PARSE_URI); + goto done; + } + + q = memchr(s, ':', p - s); + if (q) { + *host = strndup(s, q - s); + if (*host == NULL) { + err = got_error_from_errno("strndup"); + goto done; + } + if ((*host)[0] == '\0') { + err = got_error(GOT_ERR_PARSE_URI); + goto done; + } + *port = strndup(q + 1, p - (q + 1)); + if (*port == NULL) { + err = got_error_from_errno("strndup"); + goto done; + } + if ((*port)[0] == '\0') { + err = got_error(GOT_ERR_PARSE_URI); + goto done; + } + } else { + *host = strndup(s, p - s); + if (*host == NULL) { + err = got_error_from_errno("strndup"); + goto done; + } + if ((*host)[0] == '\0') { + err = got_error(GOT_ERR_PARSE_URI); + goto done; + } + } + + while (p[0] == '/' && p[1] == '/') + p++; + *request_path = strdup(p); + if (*request_path == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + got_path_strip_trailing_slashes(*request_path); + if ((*request_path)[0] == '\0') { + err = got_error(GOT_ERR_PARSE_URI); + goto done; + } +done: + if (err) { + free(*proto); + *proto = NULL; + free(*host); + *host = NULL; + free(*port); + *port = NULL; + free(*request_path); + *request_path = NULL; + } + return err; +} + +static char * +port_sprintf(int p) +{ + static char portno[32]; + int n; + + n = snprintf(portno, sizeof(portno), "%lld", (long long)p); + if (n < 0 || (size_t)n >= sizeof(portno)) + fatalx("port number too long: %lld", (long long)p); + + return portno; +} blob - e5c21b47893334094185748ac154995dee4065d6 blob + a061cd0784026046d9eed1bc09c78760626ce3e5 --- gotd/privsep_stub.c +++ gotd/privsep_stub.c @@ -64,3 +64,11 @@ got_privsep_init_pack_child(struct imsgbuf *ibuf, stru { return got_error(GOT_ERR_NOT_IMPL); } + +const struct got_error * +got_traverse_packed_commits(struct got_object_id_queue *traversed_commits, + struct got_object_id *commit_id, const char *path, + struct got_repository *repo) +{ + return NULL; +} blob - /dev/null blob + 8b7babf0b8bfda73dde136e6486c27085a6c14f4 (mode 644) --- /dev/null +++ gotd/notify.c @@ -0,0 +1,522 @@ +/* + * Copyright (c) 2024 Stefan Sperling + * + * 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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "got_error.h" +#include "got_path.h" + +#include "gotd.h" +#include "log.h" +#include "notify.h" + +#ifndef nitems +#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) +#endif + +static struct gotd_notify { + pid_t pid; + const char *title; + struct gotd_imsgev parent_iev; + struct gotd_repolist *repos; + const char *default_sender; +} gotd_notify; + +struct gotd_notify_session { + STAILQ_ENTRY(gotd_notify_session) entry; + uint32_t id; + struct gotd_imsgev iev; +}; +STAILQ_HEAD(gotd_notify_sessions, gotd_notify_session); + +static struct gotd_notify_sessions gotd_notify_sessions[GOTD_CLIENT_TABLE_SIZE]; +static SIPHASH_KEY sessions_hash_key; + +static void gotd_notify_shutdown(void); + +static uint64_t +session_hash(uint32_t session_id) +{ + return SipHash24(&sessions_hash_key, &session_id, sizeof(session_id)); +} + +static void +add_session(struct gotd_notify_session *session) +{ + uint64_t slot; + + slot = session_hash(session->id) % nitems(gotd_notify_sessions); + STAILQ_INSERT_HEAD(&gotd_notify_sessions[slot], session, entry); +} + +static struct gotd_notify_session * +find_session(uint32_t session_id) +{ + uint64_t slot; + struct gotd_notify_session *s; + + slot = session_hash(session_id) % nitems(gotd_notify_sessions); + STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) { + if (s->id == session_id) + return s; + } + + return NULL; +} + +static struct gotd_notify_session * +find_session_by_fd(int fd) +{ + uint64_t slot; + struct gotd_notify_session *s; + + for (slot = 0; slot < nitems(gotd_notify_sessions); slot++) { + STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) { + if (s->iev.ibuf.fd == fd) + return s; + } + + } + + return NULL; +} + + +static void +remove_session(struct gotd_notify_session *session) +{ + uint64_t slot; + + slot = session_hash(session->id) % nitems(gotd_notify_sessions); + STAILQ_REMOVE(&gotd_notify_sessions[slot], session, + gotd_notify_session, entry); + free(session); +} + +static uint32_t +get_session_id(void) +{ + int duplicate = 0; + uint32_t id; + + do { + id = arc4random(); + duplicate = (find_session(id) != NULL); + } while (duplicate || id == 0); + + return id; +} + +static void +gotd_notify_sighdlr(int sig, short event, void *arg) +{ + /* + * Normal signal handler rules don't apply because libevent + * decouples for us. + */ + + switch (sig) { + case SIGHUP: + log_info("%s: ignoring SIGHUP", __func__); + break; + case SIGUSR1: + log_info("%s: ignoring SIGUSR1", __func__); + break; + case SIGTERM: + case SIGINT: + gotd_notify_shutdown(); + /* NOTREACHED */ + break; + default: + fatalx("unexpected signal"); + } +} + +static void +run_notification_helper(const char *prog, const char **argv, int fd) +{ + const struct got_error *err = NULL; + pid_t pid; + int child_status; + + pid = fork(); + if (pid == -1) { + err = got_error_from_errno("fork"); + log_warn("%s", err->msg); + return; + } else if (pid == 0) { + signal(SIGQUIT, SIG_DFL); + signal(SIGINT, SIG_DFL); + signal(SIGCHLD, SIG_DFL); + + if (dup2(fd, STDIN_FILENO) == -1) { + fprintf(stderr, "%s: dup2: %s\n", getprogname(), + strerror(errno)); + _exit(1); + } + + closefrom(STDERR_FILENO + 1); + + if (execv(prog, (char *const *)argv) == -1) { + fprintf(stderr, "%s: exec %s: %s\n", getprogname(), + prog, strerror(errno)); + _exit(1); + } + + /* not reached */ + } + + if (waitpid(pid, &child_status, 0) == -1) { + err = got_error_from_errno("waitpid"); + goto done; + } + + if (!WIFEXITED(child_status)) { + err = got_error(GOT_ERR_PRIVSEP_DIED); + goto done; + } + + if (WEXITSTATUS(child_status) != 0) + err = got_error(GOT_ERR_PRIVSEP_EXIT); +done: + if (err) + log_warnx("%s: child %s pid %d: %s", gotd_notify.title, + prog, pid, err->msg); +} + +static void +notify_email(struct gotd_notification_target *target, const char *subject_line, + int fd) +{ + const char *argv[13]; + int i = 0; + + argv[i++] = GOTD_PATH_PROG_NOTIFY_EMAIL; + + argv[i++] = "-f"; + if (target->conf.email.sender) + argv[i++] = target->conf.email.sender; + else + argv[i++] = gotd_notify.default_sender; + + if (target->conf.email.responder) { + argv[i++] = "-r"; + argv[i++] = target->conf.email.responder; + } + + if (target->conf.email.hostname) { + argv[i++] = "-h"; + argv[i++] = target->conf.email.hostname; + } + + if (target->conf.email.port) { + argv[i++] = "-p"; + argv[i++] = target->conf.email.port; + } + + argv[i++] = "-s"; + argv[i++] = subject_line; + + argv[i++] = target->conf.email.recipient; + + argv[i] = NULL; + + run_notification_helper(GOTD_PATH_PROG_NOTIFY_EMAIL, argv, fd); +} + +static void +notify_http(struct gotd_notification_target *target, const char *subject_line, + int fd) +{ + const char *argv[10] = { 0 }; /* TODO */ + + run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd); +} + +static const struct got_error * +send_notification(struct imsg *imsg, struct gotd_imsgev *iev) +{ + const struct got_error *err = NULL; + struct gotd_imsg_notify inotify; + size_t datalen; + struct gotd_repo *repo; + struct gotd_notification_target *target; + int fd; + + datalen = imsg->hdr.len - IMSG_HEADER_SIZE; + if (datalen != sizeof(inotify)) + return got_error(GOT_ERR_PRIVSEP_LEN); + + memcpy(&inotify, imsg->data, datalen); + + repo = gotd_find_repo_by_name(inotify.repo_name, gotd_notify.repos); + if (repo == NULL) + return got_error(GOT_ERR_PRIVSEP_MSG); + + fd = imsg_get_fd(imsg); + if (fd == -1) + return got_error(GOT_ERR_PRIVSEP_NO_FD); + + if (lseek(fd, 0, SEEK_SET) == -1) { + err = got_error_from_errno("lseek"); + goto done; + } + + STAILQ_FOREACH(target, &repo->notification_targets, entry) { + switch (target->type) { + case GOTD_NOTIFICATION_VIA_EMAIL: + notify_email(target, inotify.subject_line, fd); + break; + case GOTD_NOTIFICATION_VIA_HTTP: + notify_http(target, inotify.subject_line, fd); + break; + } + } + + if (gotd_imsg_compose_event(iev, GOTD_IMSG_NOTIFICATION_SENT, + PROC_NOTIFY, -1, NULL, 0) == -1) { + err = got_error_from_errno("imsg compose NOTIFY"); + goto done; + } +done: + close(fd); + return err; +} + +static void +notify_dispatch_session(int fd, short event, void *arg) +{ + struct gotd_imsgev *iev = arg; + struct imsgbuf *ibuf = &iev->ibuf; + ssize_t n; + int shut = 0; + struct imsg imsg; + + if (event & EV_READ) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatal("imsg_read error"); + if (n == 0) { + /* Connection closed. */ + shut = 1; + goto done; + } + } + + if (event & EV_WRITE) { + n = msgbuf_write(&ibuf->w); + if (n == -1 && errno != EAGAIN) + fatal("msgbuf_write"); + if (n == 0) { + /* Connection closed. */ + shut = 1; + goto done; + } + } + + for (;;) { + const struct got_error *err = NULL; + + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("%s: imsg_get error", __func__); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + case GOTD_IMSG_NOTIFY: + err = send_notification(&imsg, iev); + break; + default: + log_debug("unexpected imsg %d", imsg.hdr.type); + break; + } + imsg_free(&imsg); + + if (err) + log_warnx("%s: %s", __func__, err->msg); + } +done: + if (!shut) { + gotd_imsg_event_add(iev); + } else { + struct gotd_notify_session *session; + + /* This pipe is dead. Remove its event handler */ + event_del(&iev->ev); + imsg_clear(&iev->ibuf); + + session = find_session_by_fd(fd); + if (session) + remove_session(session); + } +} + +static const struct got_error * +recv_session(struct imsg *imsg) +{ + struct gotd_notify_session *session; + size_t datalen; + int fd; + + datalen = imsg->hdr.len - IMSG_HEADER_SIZE; + if (datalen != 0) + return got_error(GOT_ERR_PRIVSEP_LEN); + + fd = imsg_get_fd(imsg); + if (fd == -1) + return got_error(GOT_ERR_PRIVSEP_NO_FD); + + session = calloc(1, sizeof(*session)); + if (session == NULL) + return got_error_from_errno("calloc"); + + session->id = get_session_id(); + imsg_init(&session->iev.ibuf, fd); + session->iev.handler = notify_dispatch_session; + session->iev.events = EV_READ; + session->iev.handler_arg = NULL; + event_set(&session->iev.ev, session->iev.ibuf.fd, EV_READ, + notify_dispatch_session, &session->iev); + gotd_imsg_event_add(&session->iev); + add_session(session); + + return NULL; +} + +static void +notify_dispatch(int fd, short event, void *arg) +{ + struct gotd_imsgev *iev = arg; + struct imsgbuf *ibuf = &iev->ibuf; + ssize_t n; + int shut = 0; + struct imsg imsg; + + if (event & EV_READ) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatal("imsg_read error"); + if (n == 0) { + /* Connection closed. */ + shut = 1; + goto done; + } + } + + if (event & EV_WRITE) { + n = msgbuf_write(&ibuf->w); + if (n == -1 && errno != EAGAIN) + fatal("msgbuf_write"); + if (n == 0) { + /* Connection closed. */ + shut = 1; + goto done; + } + } + + for (;;) { + const struct got_error *err = NULL; + + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("%s: imsg_get error", __func__); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + case GOTD_IMSG_CONNECT_SESSION: + err = recv_session(&imsg); + break; + default: + log_debug("unexpected imsg %d", imsg.hdr.type); + break; + } + imsg_free(&imsg); + + if (err) + log_warnx("%s: %s", __func__, err->msg); + } +done: + if (!shut) { + gotd_imsg_event_add(iev); + } else { + /* This pipe is dead. Remove its event handler */ + event_del(&iev->ev); + event_loopexit(NULL); + } + +} + +void +notify_main(const char *title, struct gotd_repolist *repos, + const char *default_sender) +{ + const struct got_error *err = NULL; + struct event evsigint, evsigterm, evsighup, evsigusr1; + + arc4random_buf(&sessions_hash_key, sizeof(sessions_hash_key)); + + gotd_notify.title = title; + gotd_notify.repos = repos; + gotd_notify.default_sender = default_sender; + gotd_notify.pid = getpid(); + + signal_set(&evsigint, SIGINT, gotd_notify_sighdlr, NULL); + signal_set(&evsigterm, SIGTERM, gotd_notify_sighdlr, NULL); + signal_set(&evsighup, SIGHUP, gotd_notify_sighdlr, NULL); + signal_set(&evsigusr1, SIGUSR1, gotd_notify_sighdlr, NULL); + signal(SIGPIPE, SIG_IGN); + + signal_add(&evsigint, NULL); + signal_add(&evsigterm, NULL); + signal_add(&evsighup, NULL); + signal_add(&evsigusr1, NULL); + + imsg_init(&gotd_notify.parent_iev.ibuf, GOTD_FILENO_MSG_PIPE); + gotd_notify.parent_iev.handler = notify_dispatch; + gotd_notify.parent_iev.events = EV_READ; + gotd_notify.parent_iev.handler_arg = NULL; + event_set(&gotd_notify.parent_iev.ev, gotd_notify.parent_iev.ibuf.fd, + EV_READ, notify_dispatch, &gotd_notify.parent_iev); + gotd_imsg_event_add(&gotd_notify.parent_iev); + + event_dispatch(); + + if (err) + log_warnx("%s: %s", title, err->msg); + gotd_notify_shutdown(); +} + +void +gotd_notify_shutdown(void) +{ + log_debug("shutting down"); + exit(0); +} blob - /dev/null blob + 8173549e2a96f7a2443ff8bd53806411e4312d57 (mode 644) --- /dev/null +++ gotd/notify.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024 Stefan Sperling + * + * 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. + */ + +void notify_main(const char *, struct gotd_repolist *, const char *); blob - 6c4441cc4454a3e740e917e1c8cf70fb2764235c blob + 2ff8767a91c4ae5e858bc0906a5889a4179c3f35 --- gotd/repo_write.c +++ gotd/repo_write.c @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,10 @@ #include "got_object.h" #include "got_reference.h" #include "got_path.h" +#include "got_diff.h" +#include "got_cancel.h" +#include "got_commit_graph.h" +#include "got_opentemp.h" #include "got_lib_delta.h" #include "got_lib_delta_cache.h" @@ -74,6 +79,12 @@ static struct repo_write { struct got_pathlist_head *protected_tag_namespaces; struct got_pathlist_head *protected_branch_namespaces; struct got_pathlist_head *protected_branches; + struct { + FILE *f1; + FILE *f2; + int fd1; + int fd2; + } diff; } repo_write; struct gotd_ref_update { @@ -1627,8 +1638,576 @@ receive_pack_idx(struct imsg *imsg, struct gotd_imsgev return got_error(GOT_ERR_PRIVSEP_NO_FD); return NULL; +} + +static char * +get_datestr(time_t *time, char *datebuf) +{ + struct tm mytm, *tm; + char *p, *s; + + tm = gmtime_r(time, &mytm); + if (tm == NULL) + return NULL; + s = asctime_r(tm, datebuf); + if (s == NULL) + return NULL; + p = strchr(s, '\n'); + if (p) + *p = '\0'; + return s; +} + +static const struct got_error * +notify_removed_ref(const char *refname, uint8_t *sha1, + struct gotd_imsgev *iev, int fd) +{ + const struct got_error *err; + struct got_object_id id; + char *id_str; + + memset(&id, 0, sizeof(id)); + memcpy(id.sha1, sha1, sizeof(id.sha1)); + + err = got_object_id_str(&id_str, &id); + if (err) + return err; + + dprintf(fd, "Removed %s: %s\n", refname, id_str); + free(id_str); + return err; } +static const char * +format_author(char *author) +{ + char *smallerthan; + + smallerthan = strchr(author, '<'); + if (smallerthan && smallerthan[1] != '\0') + author = smallerthan + 1; + author[strcspn(author, "@>")] = '\0'; + + return author; +} + +static const struct got_error * +print_commit_oneline(struct got_commit_object *commit, struct got_object_id *id, + struct got_repository *repo, int fd) +{ + const struct got_error *err = NULL; + char *id_str = NULL, *logmsg0 = NULL; + char *s, *nl; + char *committer = NULL, *author = NULL; + char datebuf[12]; /* YYYY-MM-DD + SPACE + NUL */ + struct tm tm; + time_t committer_time; + + err = got_object_id_str(&id_str, id); + if (err) + return err; + + committer_time = got_object_commit_get_committer_time(commit); + if (gmtime_r(&committer_time, &tm) == NULL) { + err = got_error_from_errno("gmtime_r"); + goto done; + } + if (strftime(datebuf, sizeof(datebuf), "%G-%m-%d ", &tm) == 0) { + err = got_error(GOT_ERR_NO_SPACE); + goto done; + } + + err = got_object_commit_get_logmsg(&logmsg0, commit); + if (err) + goto done; + + s = logmsg0; + while (isspace((unsigned char)s[0])) + s++; + + nl = strchr(s, '\n'); + if (nl) { + *nl = '\0'; + } + + if (strcmp(got_object_commit_get_author(commit), + got_object_commit_get_committer(commit)) != 0) { + author = strdup(got_object_commit_get_author(commit)); + if (author == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + dprintf(fd, "%s%.7s %.8s %s\n", datebuf, id_str, + format_author(author), s); + } else { + committer = strdup(got_object_commit_get_committer(commit)); + if (committer == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + dprintf(fd, "%s%.7s %.8s %s\n", datebuf, id_str, + format_author(committer), s); + } + + if (fsync(fd) == -1 && err == NULL) + err = got_error_from_errno("fsync"); +done: + free(id_str); + free(logmsg0); + free(committer); + free(author); + return err; +} + +static const struct got_error * +print_diffstat(struct got_diffstat_cb_arg *dsa, int fd) +{ + struct got_pathlist_entry *pe; + + TAILQ_FOREACH(pe, dsa->paths, entry) { + struct got_diff_changed_path *cp = pe->data; + int pad = dsa->max_path_len - pe->path_len + 1; + + dprintf(fd, " %c %s%*c | %*d+ %*d-\n", cp->status, + pe->path, pad, ' ', dsa->add_cols + 1, cp->add, + dsa->rm_cols + 1, cp->rm); + } + dprintf(fd, + "\n%d file%s changed, %d insertion%s(+), %d deletion%s(-)\n\n", + dsa->nfiles, dsa->nfiles > 1 ? "s" : "", dsa->ins, + dsa->ins != 1 ? "s" : "", dsa->del, dsa->del != 1 ? "s" : ""); + + return NULL; +} + +static const struct got_error * +print_commit(struct got_commit_object *commit, struct got_object_id *id, + struct got_repository *repo, struct got_pathlist_head *changed_paths, + struct got_diffstat_cb_arg *diffstat, int fd) +{ + const struct got_error *err = NULL; + char *id_str, *datestr, *logmsg0, *logmsg, *line; + char datebuf[26]; + time_t committer_time; + const char *author, *committer; + + err = got_object_id_str(&id_str, id); + if (err) + return err; + + dprintf(fd, "commit %s\n", id_str); + free(id_str); + id_str = NULL; + dprintf(fd, "from: %s\n", got_object_commit_get_author(commit)); + author = got_object_commit_get_author(commit); + committer = got_object_commit_get_committer(commit); + if (strcmp(author, committer) != 0) + dprintf(fd, "via: %s\n", committer); + committer_time = got_object_commit_get_committer_time(commit); + datestr = get_datestr(&committer_time, datebuf); + if (datestr) + dprintf(fd, "date: %s UTC\n", datestr); + if (got_object_commit_get_nparents(commit) > 1) { + const struct got_object_id_queue *parent_ids; + struct got_object_qid *qid; + int n = 1; + parent_ids = got_object_commit_get_parent_ids(commit); + STAILQ_FOREACH(qid, parent_ids, entry) { + err = got_object_id_str(&id_str, &qid->id); + if (err) + goto done; + dprintf(fd, "parent %d: %s\n", n++, id_str); + free(id_str); + id_str = NULL; + } + } + + err = got_object_commit_get_logmsg(&logmsg0, commit); + if (err) + goto done; + + logmsg = logmsg0; + do { + line = strsep(&logmsg, "\n"); + if (line) + dprintf(fd, " %s\n", line); + } while (line); + free(logmsg0); + + err = print_diffstat(diffstat, fd); + if (err) + goto done; + + if (fsync(fd) == -1 && err == NULL) + err = got_error_from_errno("fsync"); +done: + free(id_str); + return err; +} + +static const struct got_error * +get_changed_paths(struct got_pathlist_head *paths, + struct got_commit_object *commit, struct got_repository *repo, + struct got_diffstat_cb_arg *dsa) +{ + const struct got_error *err = NULL; + struct got_object_id *tree_id1 = NULL, *tree_id2 = NULL; + struct got_tree_object *tree1 = NULL, *tree2 = NULL; + struct got_object_qid *qid; + got_diff_blob_cb cb = got_diff_tree_collect_changed_paths; + FILE *f1 = repo_write.diff.f1, *f2 = repo_write.diff.f2; + int fd1 = repo_write.diff.fd1, fd2 = repo_write.diff.fd2; + + if (dsa) + cb = got_diff_tree_compute_diffstat; + + err = got_opentemp_truncate(f1); + if (err) + return err; + err = got_opentemp_truncate(f2); + if (err) + return err; + err = got_opentemp_truncatefd(fd1); + if (err) + return err; + err = got_opentemp_truncatefd(fd2); + if (err) + return err; + + qid = STAILQ_FIRST(got_object_commit_get_parent_ids(commit)); + if (qid != NULL) { + struct got_commit_object *pcommit; + err = got_object_open_as_commit(&pcommit, repo, + &qid->id); + if (err) + return err; + + tree_id1 = got_object_id_dup( + got_object_commit_get_tree_id(pcommit)); + if (tree_id1 == NULL) { + got_object_commit_close(pcommit); + return got_error_from_errno("got_object_id_dup"); + } + got_object_commit_close(pcommit); + + } + + if (tree_id1) { + err = got_object_open_as_tree(&tree1, repo, tree_id1); + if (err) + goto done; + } + + tree_id2 = got_object_commit_get_tree_id(commit); + err = got_object_open_as_tree(&tree2, repo, tree_id2); + if (err) + goto done; + + err = got_diff_tree(tree1, tree2, f1, f2, fd1, fd2, "", "", repo, + cb, dsa ? (void *)dsa : paths, dsa ? 1 : 0); +done: + if (tree1) + got_object_tree_close(tree1); + if (tree2) + got_object_tree_close(tree2); + free(tree_id1); + return err; +} + +static const struct got_error * +print_commits(struct got_object_id *root_id, struct got_object_id *end_id, + struct got_repository *repo, int fd) +{ + const struct got_error *err; + struct got_commit_graph *graph; + struct got_object_id_queue reversed_commits; + struct got_object_qid *qid; + struct got_commit_object *commit = NULL; + struct got_pathlist_head changed_paths; + int ncommits = 0; + const int shortlog_threshold = 50; + + STAILQ_INIT(&reversed_commits); + TAILQ_INIT(&changed_paths); + + /* XXX first-parent only for now */ + err = got_commit_graph_open(&graph, "/", 1); + if (err) + return err; + err = got_commit_graph_iter_start(graph, root_id, repo, + check_cancelled, NULL); + if (err) + goto done; + for (;;) { + struct got_object_id id; + + err = got_commit_graph_iter_next(&id, graph, repo, + check_cancelled, NULL); + if (err) { + if (err->code == GOT_ERR_ITER_COMPLETED) + err = NULL; + break; + } + + err = got_object_open_as_commit(&commit, repo, &id); + if (err) + break; + + if (end_id && got_object_id_cmp(&id, end_id) == 0) + break; + + err = got_object_qid_alloc(&qid, &id); + if (err) + break; + + STAILQ_INSERT_HEAD(&reversed_commits, qid, entry); + ncommits++; + got_object_commit_close(commit); + + if (end_id == NULL) + break; + } + + STAILQ_FOREACH(qid, &reversed_commits, entry) { + struct got_diffstat_cb_arg dsa = { 0, 0, 0, 0, 0, 0, + &changed_paths, 0, 0, GOT_DIFF_ALGORITHM_PATIENCE }; + + err = got_object_open_as_commit(&commit, repo, &qid->id); + if (err) + break; + + if (ncommits > shortlog_threshold) { + err = print_commit_oneline(commit, &qid->id, + repo, fd); + if (err) + break; + } else { + err = get_changed_paths(&changed_paths, commit, + repo, &dsa); + if (err) + break; + err = print_commit(commit, &qid->id, repo, + &changed_paths, &dsa, fd); + } + got_object_commit_close(commit); + commit = NULL; + got_pathlist_free(&changed_paths, GOT_PATHLIST_FREE_ALL); + } +done: + if (commit) + got_object_commit_close(commit); + while (!STAILQ_EMPTY(&reversed_commits)) { + qid = STAILQ_FIRST(&reversed_commits); + STAILQ_REMOVE_HEAD(&reversed_commits, entry); + got_object_qid_free(qid); + } + got_pathlist_free(&changed_paths, GOT_PATHLIST_FREE_ALL); + got_commit_graph_close(graph); + return err; +} + +static const struct got_error * +print_tag(struct got_object_id *id, + const char *refname, struct got_repository *repo, int fd) +{ + const struct got_error *err = NULL; + struct got_tag_object *tag = NULL; + const char *tagger = NULL; + char *id_str = NULL, *tagmsg0 = NULL, *tagmsg, *line, *datestr; + char datebuf[26]; + time_t tagger_time; + + err = got_object_open_as_tag(&tag, repo, id); + if (err) + return err; + + tagger = got_object_tag_get_tagger(tag); + tagger_time = got_object_tag_get_tagger_time(tag); + err = got_object_id_str(&id_str, + got_object_tag_get_object_id(tag)); + if (err) + goto done; + + dprintf(fd, "tag %s\n", refname); + dprintf(fd, "from: %s\n", tagger); + datestr = get_datestr(&tagger_time, datebuf); + if (datestr) + dprintf(fd, "date: %s UTC\n", datestr); + + switch (got_object_tag_get_object_type(tag)) { + case GOT_OBJ_TYPE_BLOB: + dprintf(fd, "object: %s %s\n", GOT_OBJ_LABEL_BLOB, id_str); + break; + case GOT_OBJ_TYPE_TREE: + dprintf(fd, "object: %s %s\n", GOT_OBJ_LABEL_TREE, id_str); + break; + case GOT_OBJ_TYPE_COMMIT: + dprintf(fd, "object: %s %s\n", GOT_OBJ_LABEL_COMMIT, id_str); + break; + case GOT_OBJ_TYPE_TAG: + dprintf(fd, "object: %s %s\n", GOT_OBJ_LABEL_TAG, id_str); + break; + default: + break; + } + + tagmsg0 = strdup(got_object_tag_get_message(tag)); + if (tagmsg0 == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + tagmsg = tagmsg0; + do { + line = strsep(&tagmsg, "\n"); + if (line) + dprintf(fd, " %s\n", line); + } while (line); + free(tagmsg0); +done: + if (tag) + got_object_tag_close(tag); + free(id_str); + return err; +} + +static const struct got_error * +notify_changed_ref(const char *refname, uint8_t *old_sha1, + uint8_t *new_sha1, struct gotd_imsgev *iev, int fd) +{ + const struct got_error *err; + struct got_object_id old_id, new_id; + int old_obj_type, new_obj_type; + const char *label; + char *new_id_str = NULL; + + memset(&old_id, 0, sizeof(old_id)); + memcpy(old_id.sha1, old_sha1, sizeof(old_id.sha1)); + memset(&new_id, 0, sizeof(new_id)); + memcpy(new_id.sha1, new_sha1, sizeof(new_id.sha1)); + + err = got_object_get_type(&old_obj_type, repo_write.repo, &old_id); + if (err) + return err; + + err = got_object_get_type(&new_obj_type, repo_write.repo, &new_id); + if (err) + return err; + + switch (new_obj_type) { + case GOT_OBJ_TYPE_COMMIT: + err = print_commits(&new_id, + old_obj_type == GOT_OBJ_TYPE_COMMIT ? &old_id : NULL, + repo_write.repo, fd); + break; + case GOT_OBJ_TYPE_TAG: + err = print_tag(&new_id, refname, repo_write.repo, fd); + break; + default: + err = got_object_type_label(&label, new_obj_type); + if (err) + goto done; + err = got_object_id_str(&new_id_str, &new_id); + if (err) + goto done; + dprintf(fd, "%s: %s object %s\n", refname, label, new_id_str); + break; + } +done: + free(new_id_str); + return err; +} + +static const struct got_error * +notify_created_ref(const char *refname, uint8_t *sha1, + struct gotd_imsgev *iev, int fd) +{ + const struct got_error *err; + struct got_object_id id; + int obj_type; + + memset(&id, 0, sizeof(id)); + memcpy(id.sha1, sha1, sizeof(id.sha1)); + + err = got_object_get_type(&obj_type, repo_write.repo, &id); + if (err) + return err; + + if (obj_type == GOT_OBJ_TYPE_TAG) + return print_tag(&id, refname, repo_write.repo, fd); + + return print_commits(&id, NULL, repo_write.repo, fd); +} + +static const struct got_error * +render_notification(struct imsg *imsg, struct gotd_imsgev *iev) +{ + const struct got_error *err = NULL; + struct gotd_imsg_notification_content ireq; + size_t datalen, len; + char *refname; + struct ibuf *wbuf; + int fd; + + fd = imsg_get_fd(imsg); + if (fd == -1) + return got_error(GOT_ERR_PRIVSEP_NO_FD); + + datalen = imsg->hdr.len - IMSG_HEADER_SIZE; + if (datalen < sizeof(ireq)) + return got_error(GOT_ERR_PRIVSEP_LEN); + + memcpy(&ireq, imsg->data, sizeof(ireq)); + + if (datalen != sizeof(ireq) + ireq.refname_len) + return got_error(GOT_ERR_PRIVSEP_LEN); + + refname = strndup(imsg->data + sizeof(ireq), ireq.refname_len); + if (refname == NULL) + return got_error_from_errno("strndup"); + + switch (ireq.action) { + case GOTD_NOTIF_ACTION_CREATED: + err = notify_created_ref(refname, ireq.new_id, iev, fd); + break; + case GOTD_NOTIF_ACTION_REMOVED: + err = notify_removed_ref(refname, ireq.old_id, iev, fd); + break; + case GOTD_NOTIF_ACTION_CHANGED: + err = notify_changed_ref(refname, ireq.old_id, ireq.new_id, + iev, fd); + break; + } + + if (fsync(fd) == -1) { + err = got_error_from_errno("fsync"); + goto done; + } + + len = sizeof(ireq) + ireq.refname_len; + wbuf = imsg_create(&iev->ibuf, GOTD_IMSG_NOTIFY, PROC_REPO_WRITE, + repo_write.pid, len); + if (wbuf == NULL) { + err = got_error_from_errno("imsg_create REF"); + goto done; + } + if (imsg_add(wbuf, &ireq, sizeof(ireq)) == -1) { + err = got_error_from_errno("imsg_add NOTIFY"); + goto done; + } + if (imsg_add(wbuf, refname, ireq.refname_len) == -1) { + err = got_error_from_errno("imsg_add NOTIFY"); + goto done; + } + + imsg_close(&iev->ibuf, wbuf); + gotd_imsg_event_add(iev); +done: + free(refname); + if (close(fd) == -1 && err == NULL) + err = got_error_from_errno("close"); + return err; +} + static void repo_write_dispatch_session(int fd, short event, void *arg) { @@ -1725,6 +2304,13 @@ repo_write_dispatch_session(int fd, short event, void err = update_refs(iev); if (err) { log_warnx("update refs: %s", err->msg); + } + break; + case GOTD_IMSG_NOTIFY: + err = render_notification(&imsg, iev); + if (err) { + log_warnx("render notification: %s", err->msg); + shut = 1; } break; default: @@ -1840,6 +2426,7 @@ repo_write_dispatch(int fd, short event, void *arg) void repo_write_main(const char *title, const char *repo_path, int *pack_fds, int *temp_fds, + FILE *diff_f1, FILE *diff_f2, int diff_fd1, int diff_fd2, struct got_pathlist_head *protected_tag_namespaces, struct got_pathlist_head *protected_branch_namespaces, struct got_pathlist_head *protected_branches) @@ -1862,6 +2449,10 @@ repo_write_main(const char *title, const char *repo_pa repo_write.protected_tag_namespaces = protected_tag_namespaces; repo_write.protected_branch_namespaces = protected_branch_namespaces; repo_write.protected_branches = protected_branches; + repo_write.diff.f1 = diff_f1; + repo_write.diff.f2 = diff_f2; + repo_write.diff.fd1 = diff_fd1; + repo_write.diff.fd2 = diff_fd2; STAILQ_INIT(&repo_write_client.ref_updates); @@ -1894,6 +2485,14 @@ repo_write_main(const char *title, const char *repo_pa event_dispatch(); done: + if (fclose(diff_f1) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + if (fclose(diff_f2) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + if (close(diff_fd1) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (close(diff_fd2) == -1 && err == NULL) + err = got_error_from_errno("close"); if (err) log_warnx("%s: %s", title, err->msg); repo_write_shutdown(); blob - e8192eec3947ce83dcedba9e20048cb0ff7dfc76 blob + 6d09a0d009bafa745ad7d7fa491e5c6bcfe4149e --- gotd/repo_write.h +++ gotd/repo_write.h @@ -15,6 +15,7 @@ */ void repo_write_main(const char *, const char *, int *, int *, + FILE *, FILE *, int, int, struct got_pathlist_head *, struct got_pathlist_head *, struct got_pathlist_head *); void repo_write_shutdown(void); blob - d67328a67243a5beff513f9e3feeb89708060657 blob + 163f482824dd1bbb39949c76de672eb8682b553a --- gotd/session.c +++ gotd/session.c @@ -53,14 +53,25 @@ #include "log.h" #include "session.h" +struct gotd_session_notif { + STAILQ_ENTRY(gotd_session_notif) entry; + int fd; + enum gotd_notification_action action; + char *refname; + struct got_object_id old_id; + struct got_object_id new_id; +}; +STAILQ_HEAD(gotd_session_notifications, gotd_session_notif) notifications; static struct gotd_session { pid_t pid; const char *title; struct got_repository *repo; + struct gotd_repo *repo_cfg; int *pack_fds; int *temp_fds; struct gotd_imsgev parent_iev; + struct gotd_imsgev notifier_iev; struct timeval request_timeout; enum gotd_procid proc_id; } gotd_session; @@ -79,6 +90,7 @@ static struct gotd_session_client { struct event tmo; uid_t euid; gid_t egid; + char *username; char *packfile_path; char *packidx_path; int nref_updates; @@ -397,10 +409,253 @@ begin_ref_updates(struct gotd_session_client *client, return got_error(GOT_ERR_PRIVSEP_MSG); client->nref_updates = istart.nref_updates; + return NULL; +} + +static const struct got_error * +validate_namespace(const char *namespace) +{ + size_t len = strlen(namespace); + + if (len < 5 || strncmp("refs/", namespace, 5) != 0 || + namespace[len - 1] != '/') { + return got_error_fmt(GOT_ERR_BAD_REF_NAME, + "reference namespace '%s'", namespace); + } + return NULL; } static const struct got_error * +queue_notification(struct got_object_id *old_id, struct got_object_id *new_id, + struct got_repository *repo, struct got_reference *ref) +{ + const struct got_error *err = NULL; + struct gotd_session_client *client = &gotd_session_client; + struct gotd_repo *repo_cfg = gotd_session.repo_cfg; + struct gotd_imsgev *iev = &client->repo_child_iev; + struct got_pathlist_entry *pe; + struct gotd_session_notif *notif; + + if (iev->ibuf.fd == -1 || + STAILQ_EMPTY(&repo_cfg->notification_targets)) + return NULL; /* notifications unused */ + + TAILQ_FOREACH(pe, &repo_cfg->notification_refs, entry) { + const char *refname = pe->path; + if (strcmp(got_ref_get_name(ref), refname) == 0) + break; + } + if (pe == NULL) { + TAILQ_FOREACH(pe, &repo_cfg->notification_ref_namespaces, + entry) { + const char *namespace = pe->path; + + err = validate_namespace(namespace); + if (err) + return err; + if (strncmp(namespace, got_ref_get_name(ref), + strlen(namespace)) == 0) + break; + } + } + + /* + * If a branch or a reference namespace was specified in the + * configuration file then only send notifications if a match + * was found. + */ + if (pe == NULL && (!TAILQ_EMPTY(&repo_cfg->notification_refs) || + !TAILQ_EMPTY(&repo_cfg->notification_ref_namespaces))) + return NULL; + + notif = calloc(1, sizeof(*notif)); + if (notif == NULL) + return got_error_from_errno("calloc"); + + notif->fd = -1; + + if (old_id == NULL) + notif->action = GOTD_NOTIF_ACTION_CREATED; + else if (new_id == NULL) + notif->action = GOTD_NOTIF_ACTION_REMOVED; + else + notif->action = GOTD_NOTIF_ACTION_CHANGED; + + if (old_id != NULL) + memcpy(¬if->old_id, old_id, sizeof(notif->old_id)); + if (new_id != NULL) + memcpy(¬if->new_id, new_id, sizeof(notif->new_id)); + + notif->refname = strdup(got_ref_get_name(ref)); + if (notif->refname == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + + STAILQ_INSERT_TAIL(¬ifications, notif, entry); +done: + if (err && notif) { + free(notif->refname); + free(notif); + } + return err; +} + +/* Forward notification content to the NOTIFY process. */ +static const struct got_error * +forward_notification(struct gotd_session_client *client, struct imsg *imsg) +{ + const struct got_error *err = NULL; + struct gotd_imsgev *iev = &gotd_session.notifier_iev; + struct gotd_session_notif *notif; + struct gotd_imsg_notification_content icontent; + char *refname = NULL; + size_t datalen; + struct gotd_imsg_notify inotify; + const char *action; + + memset(&inotify, 0, sizeof(inotify)); + + datalen = imsg->hdr.len - IMSG_HEADER_SIZE; + if (datalen < sizeof(icontent)) + return got_error(GOT_ERR_PRIVSEP_LEN); + memcpy(&icontent, imsg->data, sizeof(icontent)); + if (datalen != sizeof(icontent) + icontent.refname_len) + return got_error(GOT_ERR_PRIVSEP_LEN); + refname = strndup(imsg->data + sizeof(icontent), icontent.refname_len); + if (refname == NULL) + return got_error_from_errno("strndup"); + + notif = STAILQ_FIRST(¬ifications); + if (notif == NULL) + return got_error(GOT_ERR_PRIVSEP_MSG); + + STAILQ_REMOVE(¬ifications, notif, gotd_session_notif, entry); + + if (notif->action != icontent.action || notif->fd == -1 || + strcmp(notif->refname, refname) != 0) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + if (notif->action == GOTD_NOTIF_ACTION_CREATED) { + if (memcmp(notif->new_id.sha1, icontent.new_id, + SHA1_DIGEST_LENGTH) != 0) { + err = got_error_msg(GOT_ERR_PRIVSEP_MSG, + "received notification content for unknown event"); + goto done; + } + } else if (notif->action == GOTD_NOTIF_ACTION_REMOVED) { + if (memcmp(notif->old_id.sha1, icontent.old_id, + SHA1_DIGEST_LENGTH) != 0) { + err = got_error_msg(GOT_ERR_PRIVSEP_MSG, + "received notification content for unknown event"); + goto done; + } + } else if (memcmp(notif->old_id.sha1, icontent.old_id, + SHA1_DIGEST_LENGTH) != 0 || + memcmp(notif->new_id.sha1, icontent.new_id, + SHA1_DIGEST_LENGTH) != 0) { + err = got_error_msg(GOT_ERR_PRIVSEP_MSG, + "received notification content for unknown event"); + goto done; + } + + switch (notif->action) { + case GOTD_NOTIF_ACTION_CREATED: + action = "created"; + break; + case GOTD_NOTIF_ACTION_REMOVED: + action = "removed"; + break; + case GOTD_NOTIF_ACTION_CHANGED: + action = "changed"; + break; + default: + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + + strlcpy(inotify.repo_name, gotd_session.repo_cfg->name, + sizeof(inotify.repo_name)); + + snprintf(inotify.subject_line, sizeof(inotify.subject_line), + "%s: %s %s %s", gotd_session.repo_cfg->name, + client->username, action, notif->refname); + + if (gotd_imsg_compose_event(iev, GOTD_IMSG_NOTIFY, + PROC_SESSION_WRITE, notif->fd, &inotify, sizeof(inotify)) + == -1) { + err = got_error_from_errno("imsg compose NOTIFY"); + goto done; + } + notif->fd = -1; +done: + if (notif->fd != -1) + close(notif->fd); + free(notif); + free(refname); + return err; +} + +/* Request notification content from REPO_WRITE process. */ +static const struct got_error * +request_notification(struct gotd_session_notif *notif) +{ + const struct got_error *err = NULL; + struct gotd_session_client *client = &gotd_session_client; + struct gotd_imsgev *iev = &client->repo_child_iev; + struct gotd_imsg_notification_content icontent; + struct ibuf *wbuf; + size_t len; + int fd; + + fd = got_opentempfd(); + if (fd == -1) + return got_error_from_errno("got_opentemp"); + + memset(&icontent, 0, sizeof(icontent)); + icontent.client_id = client->id; + + icontent.action = notif->action; + memcpy(&icontent.old_id, ¬if->old_id, sizeof(notif->old_id)); + memcpy(&icontent.new_id, ¬if->new_id, sizeof(notif->new_id)); + icontent.refname_len = strlen(notif->refname); + + len = sizeof(icontent) + icontent.refname_len; + wbuf = imsg_create(&iev->ibuf, GOTD_IMSG_NOTIFY, + gotd_session.proc_id, gotd_session.pid, len); + if (wbuf == NULL) { + err = got_error_from_errno("imsg_create NOTIFY"); + goto done; + } + if (imsg_add(wbuf, &icontent, sizeof(icontent)) == -1) { + err = got_error_from_errno("imsg_add NOTIFY"); + goto done; + } + if (imsg_add(wbuf, notif->refname, icontent.refname_len) == -1) { + err = got_error_from_errno("imsg_add NOTIFY"); + goto done; + } + + notif->fd = dup(fd); + if (notif->fd == -1) { + err = got_error_from_errno("dup"); + goto done; + } + + ibuf_fd_set(wbuf, fd); + fd = -1; + + imsg_close(&iev->ibuf, wbuf); + gotd_imsg_event_add(iev); +done: + if (err && fd != -1) + close(fd); + return err; +} + +static const struct got_error * update_ref(int *shut, struct gotd_session_client *client, const char *repo_path, struct imsg *imsg) { @@ -409,6 +664,7 @@ update_ref(int *shut, struct gotd_session_client *clie struct got_reference *ref = NULL; struct gotd_imsg_ref_update iref; struct got_object_id old_id, new_id; + struct gotd_session_notif *notif; struct got_object_id *id = NULL; char *refname = NULL; size_t datalen; @@ -451,6 +707,9 @@ update_ref(int *shut, struct gotd_session_client *clie err = got_ref_write(ref, repo); /* will lock/unlock */ if (err) goto done; + err = queue_notification(NULL, &new_id, repo, ref); + if (err) + goto done; } else { err = got_ref_resolve(&id, repo, ref); if (err) @@ -490,7 +749,9 @@ update_ref(int *shut, struct gotd_session_client *clie err = got_ref_delete(ref, repo); if (err) goto done; - + err = queue_notification(&old_id, NULL, repo, ref); + if (err) + goto done; free(id); id = NULL; } else { @@ -519,10 +780,12 @@ update_ref(int *shut, struct gotd_session_client *clie err = got_ref_change_ref(ref, &new_id); if (err) goto done; - err = got_ref_write(ref, repo); if (err) goto done; + err = queue_notification(&old_id, &new_id, repo, ref); + if (err) + goto done; } free(id); @@ -543,7 +806,17 @@ done: client->nref_updates--; if (client->nref_updates == 0) { send_refs_updated(client); - client->flush_disconnect = 1; + notif = STAILQ_FIRST(¬ifications); + if (notif) { + client->state = GOTD_STATE_NOTIFY; + err = request_notification(notif); + if (err) { + log_warn("could not send notification: " + "%s", err->msg); + client->flush_disconnect = 1; + } + } else + client->flush_disconnect = 1; } } @@ -560,6 +833,21 @@ done: return err; } +static const struct got_error * +recv_notification_content(uint32_t *client_id, struct imsg *imsg) +{ + struct gotd_imsg_notification_content inotif; + size_t datalen; + + datalen = imsg->hdr.len - IMSG_HEADER_SIZE; + if (datalen < sizeof(inotif)) + return got_error(GOT_ERR_PRIVSEP_LEN); + memcpy(&inotif, imsg->data, sizeof(inotif)); + + *client_id = inotif.client_id; + return NULL; +} + static void session_dispatch_repo_child(int fd, short event, void *arg) { @@ -596,7 +884,7 @@ session_dispatch_repo_child(int fd, short event, void uint32_t client_id = 0; int do_disconnect = 0; int do_ref_updates = 0, do_ref_update = 0; - int do_packfile_install = 0; + int do_packfile_install = 0, do_notify = 0; if ((n = imsg_get(ibuf, &imsg)) == -1) fatal("%s: imsg_get error", __func__); @@ -627,6 +915,11 @@ session_dispatch_repo_child(int fd, short event, void if (err == NULL) do_ref_update = 1; break; + case GOTD_IMSG_NOTIFY: + err = recv_notification_content(&client_id, &imsg); + if (err == NULL) + do_notify = 1; + break; default: log_debug("unexpected imsg %d", imsg.hdr.type); break; @@ -638,6 +931,8 @@ session_dispatch_repo_child(int fd, short event, void else disconnect(client); } else { + struct gotd_session_notif *notif; + if (do_packfile_install) err = install_pack(client, gotd_session.repo->path, &imsg); @@ -646,8 +941,21 @@ session_dispatch_repo_child(int fd, short event, void else if (do_ref_update) err = update_ref(&shut, client, gotd_session.repo->path, &imsg); + else if (do_notify) + err = forward_notification(client, &imsg); if (err) log_warnx("uid %d: %s", client->euid, err->msg); + + notif = STAILQ_FIRST(¬ifications); + if (notif && do_notify) { + /* Request content for next notification. */ + err = request_notification(notif); + if (err) { + log_warn("could not send notification: " + "%s", err->msg); + shut = 1; + } + } } imsg_free(&imsg); } @@ -1295,9 +1603,12 @@ recv_connect(struct imsg *imsg) return got_error(GOT_ERR_PRIVSEP_MSG); datalen = imsg->hdr.len - IMSG_HEADER_SIZE; - if (datalen != sizeof(iconnect)) + if (datalen < sizeof(iconnect)) return got_error(GOT_ERR_PRIVSEP_LEN); memcpy(&iconnect, imsg->data, sizeof(iconnect)); + if (iconnect.username_len == 0 || + datalen != sizeof(iconnect) + iconnect.username_len) + return got_error(GOT_ERR_PRIVSEP_LEN); client->euid = iconnect.euid; client->egid = iconnect.egid; @@ -1305,6 +1616,11 @@ recv_connect(struct imsg *imsg) if (client->fd == -1) return got_error(GOT_ERR_PRIVSEP_NO_FD); + client->username = strndup(imsg->data + sizeof(iconnect), + iconnect.username_len); + if (client->username == NULL) + return got_error_from_errno("strndup"); + imsg_init(&client->iev.ibuf, client->fd); client->iev.handler = session_dispatch_client; client->iev.events = EV_READ; @@ -1317,7 +1633,117 @@ recv_connect(struct imsg *imsg) return NULL; } +static void +session_dispatch_notifier(int fd, short event, void *arg) +{ + const struct got_error *err; + struct gotd_session_client *client = &gotd_session_client; + struct gotd_imsgev *iev = arg; + struct imsgbuf *ibuf = &iev->ibuf; + ssize_t n; + int shut = 0; + struct imsg imsg; + struct gotd_session_notif *notif; + + if (event & EV_READ) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatal("imsg_read error"); + if (n == 0) { + /* Connection closed. */ + shut = 1; + goto done; + } + } + + if (event & EV_WRITE) { + n = msgbuf_write(&ibuf->w); + if (n == -1 && errno != EAGAIN) + fatal("msgbuf_write"); + if (n == 0) { + /* Connection closed. */ + shut = 1; + goto done; + } + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("%s: imsg_get error", __func__); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + case GOTD_IMSG_NOTIFICATION_SENT: + if (client->state != GOTD_STATE_NOTIFY) { + log_warn("unexpected imsg %d", imsg.hdr.type); + break; + } + notif = STAILQ_FIRST(¬ifications); + if (notif == NULL) { + disconnect(client); + break; /* NOTREACHED */ + } + /* Request content for the next notification. */ + err = request_notification(notif); + if (err) { + log_warn("could not send notification: %s", + err->msg); + disconnect(client); + } + break; + default: + log_debug("unexpected imsg %d", imsg.hdr.type); + break; + } + + imsg_free(&imsg); + } +done: + if (!shut) { + gotd_imsg_event_add(iev); + } else { + /* This pipe is dead. Remove its event handler */ + event_del(&iev->ev); + imsg_clear(&iev->ibuf); + imsg_init(&iev->ibuf, -1); + } +} + static const struct got_error * +recv_notifier(struct imsg *imsg) +{ + struct gotd_imsgev *iev = &gotd_session.notifier_iev; + struct gotd_session_client *client = &gotd_session_client; + size_t datalen; + int fd; + + if (client->state != GOTD_STATE_EXPECT_LIST_REFS) + return got_error(GOT_ERR_PRIVSEP_MSG); + + /* We should already have received a pipe to the listener. */ + if (client->fd == -1) + return got_error(GOT_ERR_PRIVSEP_MSG); + + datalen = imsg->hdr.len - IMSG_HEADER_SIZE; + if (datalen != 0) + return got_error(GOT_ERR_PRIVSEP_LEN); + + fd = imsg_get_fd(imsg); + if (fd == -1) + return NULL; /* notifications unused */ + + imsg_init(&iev->ibuf, fd); + iev->handler = session_dispatch_notifier; + iev->events = EV_READ; + iev->handler_arg = NULL; + event_set(&iev->ev, iev->ibuf.fd, EV_READ, + session_dispatch_notifier, iev); + gotd_imsg_event_add(iev); + + return NULL; +} + +static const struct got_error * recv_repo_child(struct imsg *imsg) { struct gotd_imsg_connect_repo_child ichild; @@ -1418,6 +1844,9 @@ session_dispatch(int fd, short event, void *arg) case GOTD_IMSG_DISCONNECT: do_disconnect = 1; break; + case GOTD_IMSG_CONNECT_NOTIFIER: + err = recv_notifier(&imsg); + break; case GOTD_IMSG_CONNECT_REPO_CHILD: err = recv_repo_child(&imsg); if (err) @@ -1454,19 +1883,24 @@ done: void session_main(const char *title, const char *repo_path, int *pack_fds, int *temp_fds, struct timeval *request_timeout, - enum gotd_procid proc_id) + struct gotd_repo *repo_cfg, enum gotd_procid proc_id) { const struct got_error *err = NULL; struct event evsigint, evsigterm, evsighup, evsigusr1; + STAILQ_INIT(¬ifications); + gotd_session.title = title; gotd_session.pid = getpid(); gotd_session.pack_fds = pack_fds; gotd_session.temp_fds = temp_fds; memcpy(&gotd_session.request_timeout, request_timeout, sizeof(gotd_session.request_timeout)); + gotd_session.repo_cfg = repo_cfg; gotd_session.proc_id = proc_id; + imsg_init(&gotd_session.notifier_iev.ibuf, -1); + err = got_repo_open(&gotd_session.repo, repo_path, NULL, pack_fds); if (err) goto done; @@ -1518,10 +1952,23 @@ done: void gotd_session_shutdown(void) { + struct gotd_session_notif *notif; + log_debug("shutting down"); + + while (!STAILQ_EMPTY(¬ifications)) { + notif = STAILQ_FIRST(¬ifications); + STAILQ_REMOVE_HEAD(¬ifications, entry); + if (notif->fd != -1) + close(notif->fd); + free(notif->refname); + free(notif); + } + if (gotd_session.repo) got_repo_close(gotd_session.repo); got_repo_pack_fds_close(gotd_session.pack_fds); got_repo_temp_fds_close(gotd_session.temp_fds); + free(gotd_session_client.username); exit(0); } blob - de20117ce268c646687a1977e8e0c7086a7fb2d6 blob + 624f7bb81035da8f1d5293bcbcdf72cd6f064102 --- gotd/session.h +++ gotd/session.h @@ -15,4 +15,4 @@ */ void session_main(const char *, const char *, int *, int *, struct timeval *, - enum gotd_procid); + struct gotd_repo *, enum gotd_procid); blob - ddfc0f1e9d1f0c373066bc33b387387e5beeaf0d blob + eaca641d2887c0f5e66a410257eaf3fae004a13d --- regress/gotd/Makefile +++ regress/gotd/Makefile @@ -4,7 +4,7 @@ REGRESS_TARGETS=test_repo_read test_repo_read_group \ 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_repo_write_protected test_email_notification NOOBJ=Yes CLEANFILES=gotd.conf @@ -14,7 +14,9 @@ GOTD_TEST_ROOT=/tmp GOTD_DEVUSER?=gotdev GOTD_DEVUSER_HOME!=userinfo $(GOTD_DEVUSER) | awk '/^dir/ {print $$2}' GOTD_TEST_REPO!?=mktemp -d "$(GOTD_TEST_ROOT)/gotd-test-repo-XXXXXXXXXX" -GOTD_TEST_REPO_URL=ssh://${GOTD_DEVUSER}@127.0.0.1/test-repo +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_USER?=${DOAS_USER} .if empty(GOTD_TEST_USER) @@ -37,15 +39,20 @@ PREFIX ?= ${GOTD_TEST_USER_HOME} BINDIR ?= ${PREFIX}/bin .endif -GOTD_START_CMD?=$(BINDIR)/gotd -vv -f $(PWD)/gotd.conf +GOTD_START_CMD?=env ${GOTD_ENV} $(BINDIR)/gotd -vv -f $(PWD)/gotd.conf GOTD_STOP_CMD?=$(BINDIR)/gotctl -f $(GOTD_SOCK) stop GOTD_TRAP=trap "$(GOTD_STOP_CMD)" HUP INT QUIT PIPE TERM +GOTD_ENV=GOT_NOTIFY_EMAIL_TIMEOUT=1 + GOTD_TEST_ENV=GOTD_TEST_ROOT=$(GOTD_TEST_ROOT) \ GOTD_TEST_REPO_URL=$(GOTD_TEST_REPO_URL) \ + GOTD_TEST_REPO_NAME=$(GOTD_TEST_REPO_NAME) \ GOTD_TEST_REPO=$(GOTD_TEST_REPO) \ GOTD_SOCK=$(GOTD_SOCK) \ GOTD_DEVUSER=$(GOTD_DEVUSER) \ + GOTD_USER=$(GOTD_USER) \ + GOTD_TEST_SMTP_PORT=$(GOTD_TEST_SMTP_PORT) \ HOME=$(GOTD_TEST_USER_HOME) \ PATH=$(GOTD_TEST_USER_HOME)/bin:$(PATH) @@ -148,6 +155,20 @@ start_gotd_rw_protected: ensure_root @$(GOTD_TRAP); $(GOTD_START_CMD) @$(GOTD_TRAP); sleep .5 +start_gotd_email_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 -n ' email to ${GOTD_DEVUSER}' >> $(PWD)/gotd.conf + @echo ' relay 127.0.0.1 port ${GOTD_TEST_SMTP_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' @@ -215,4 +236,9 @@ test_request_bad: prepare_test_repo_empty start_gotd_r 'env $(GOTD_TEST_ENV) sh ./request_bad.sh' @$(GOTD_STOP_CMD) 2>/dev/null +test_email_notification: prepare_test_repo start_gotd_email_notification + @-$(GOTD_TRAP); su -m ${GOTD_TEST_USER} -c \ + 'env $(GOTD_TEST_ENV) sh ./email_notification.sh' + @$(GOTD_STOP_CMD) 2>/dev/null + .include blob - d75faae64b46a7c347ecb77c6631207a3ded44b0 blob + e91075f7971646d565d0d0c8b6509d711fc2128f --- regress/gotd/README +++ regress/gotd/README @@ -56,3 +56,9 @@ The server test suite can now be run from the top-leve The suite must be started as root in order to be able to start and stop gotd. The test suite switches to non-root users as appropriate. + +The test suite uses netcat on port 2525 to test SMTP notifications. +If this port is already in use then affected tests might fail. +If needed the port can be overriden on the make command line: + + $ doas make server-regress GOTD_TEST_SMTP_PORT=12345 blob - /dev/null blob + 44052114a99b0f98370a006d3e3c691db559ceec (mode 644) --- /dev/null +++ regress/gotd/email_notification.sh @@ -0,0 +1,533 @@ +#!/bin/sh +# +# Copyright (c) 2024 Stefan Sperling +# +# 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` + + (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \ + | timeout 5 nc -l "$GOTD_TEST_SMTP_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 nc -l + + HOSTNAME=`hostname` + printf "HELO localhost\r\n" > $testroot/stdout.expected + printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \ + >> $testroot/stdout.expected + printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected + printf "DATA\r\n" >> $testroot/stdout.expected + printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" >> $testroot/stdout.expected + printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected + printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected + printf "${GOTD_DEVUSER} changed refs/heads/main\r\n" \ + >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + printf "commit $commit_id\n" >> $testroot/stdout.expected + printf "from: $GOT_AUTHOR\n" >> $testroot/stdout.expected + d=`date -u -r $author_time +"%a %b %e %X %Y UTC"` + printf "date: $d\n" >> $testroot/stdout.expected + printf " \n" >> $testroot/stdout.expected + printf " make changes\n \n" >> $testroot/stdout.expected + printf " M alpha | 1+ 1-\n\n" >> $testroot/stdout.expected + printf "1 file changed, 1 insertion(+), 1 deletion(-)\n\n" \ + >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + printf ".\r\n" >> $testroot/stdout.expected + printf "QUIT\r\n" >> $testroot/stdout.expected + + grep -v ^Date $testroot/stdout > $testroot/stdout.filtered + cmp -s $testroot/stdout.expected $testroot/stdout.filtered + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout.filtered + 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 + + (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \ + | timeout 5 nc -l "$GOTD_TEST_SMTP_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 nc -l + + HOSTNAME=`hostname` + printf "HELO localhost\r\n" > $testroot/stdout.expected + printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \ + >> $testroot/stdout.expected + printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected + printf "DATA\r\n" >> $testroot/stdout.expected + printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" \ + >> $testroot/stdout.expected + printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected + printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected + printf "${GOTD_DEVUSER} changed refs/heads/main\r\n" \ + >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + 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 "commit $commit_id\n" >> $testroot/stdout.expected + printf "from: $GOT_AUTHOR\n" >> $testroot/stdout.expected + printf "date: $commit_time\n" >> $testroot/stdout.expected + printf " \n" >> $testroot/stdout.expected + printf " make changes\n \n" >> $testroot/stdout.expected + printf " M alpha | 1+ 1-\n\n" \ + >> $testroot/stdout.expected + printf "1 file changed, 1 insertion(+), 1 deletion(-)\n\n" \ + >> $testroot/stdout.expected + done + printf "\r\n" >> $testroot/stdout.expected + printf ".\r\n" >> $testroot/stdout.expected + printf "QUIT\r\n" >> $testroot/stdout.expected + + grep -v ^Date $testroot/stdout > $testroot/stdout.filtered + cmp -s $testroot/stdout.expected $testroot/stdout.filtered + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout.filtered + 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 + + (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \ + | timeout 5 nc -l "$GOTD_TEST_SMTP_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 nc -l + + HOSTNAME=`hostname` + printf "HELO localhost\r\n" > $testroot/stdout.expected + printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \ + >> $testroot/stdout.expected + printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected + printf "DATA\r\n" >> $testroot/stdout.expected + printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" \ + >> $testroot/stdout.expected + printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected + printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected + printf "${GOTD_DEVUSER} changed refs/heads/main\r\n" \ + >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + 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 "$commit_time $commit_id $GOT_AUTHOR_8 make changes\n" \ + >> $testroot/stdout.expected + done + printf "\r\n" >> $testroot/stdout.expected + printf ".\r\n" >> $testroot/stdout.expected + printf "QUIT\r\n" >> $testroot/stdout.expected + + grep -v ^Date $testroot/stdout > $testroot/stdout.filtered + cmp -s $testroot/stdout.expected $testroot/stdout.filtered + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout.filtered + test_done "$testroot" "$ret" + return 1 + fi + + test_done "$testroot" "$ret" +} + +test_branch_created() { + local testroot=`test_init branch_created 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 + + (cd $testroot/wt && got branch newbranch > /dev/null) + + echo "change alpha on branch" > $testroot/wt/alpha + (cd $testroot/wt && got commit -m 'newbranch' > /dev/null) + local commit_id=`git_show_branch_head $testroot/repo-clone newbranch` + local author_time=`git_show_author_time $testroot/repo-clone $commit_id` + + (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \ + | timeout 5 nc -l "$GOTD_TEST_SMTP_PORT" > $testroot/stdout) & + + got send -b newbranch -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 nc -l + + HOSTNAME=`hostname` + printf "HELO localhost\r\n" > $testroot/stdout.expected + printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \ + >> $testroot/stdout.expected + printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected + printf "DATA\r\n" >> $testroot/stdout.expected + printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" >> $testroot/stdout.expected + printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected + printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected + printf "${GOTD_DEVUSER} created refs/heads/newbranch\r\n" \ + >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + printf "commit $commit_id\n" >> $testroot/stdout.expected + printf "from: $GOT_AUTHOR\n" >> $testroot/stdout.expected + d=`date -u -r $author_time +"%a %b %e %X %Y UTC"` + printf "date: $d\n" >> $testroot/stdout.expected + printf " \n" >> $testroot/stdout.expected + printf " newbranch\n \n" >> $testroot/stdout.expected + printf " M alpha | 1+ 1-\n\n" >> $testroot/stdout.expected + printf "1 file changed, 1 insertion(+), 1 deletion(-)\n\n" \ + >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + printf ".\r\n" >> $testroot/stdout.expected + printf "QUIT\r\n" >> $testroot/stdout.expected + + grep -v ^Date $testroot/stdout > $testroot/stdout.filtered + cmp -s $testroot/stdout.expected $testroot/stdout.filtered + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout.filtered + test_done "$testroot" "$ret" + return 1 + fi + + test_done "$testroot" "$ret" +} + +test_branch_removed() { + local testroot=`test_init branch_removed 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 + + (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \ + | timeout 5 nc -l "$GOTD_TEST_SMTP_PORT" > $testroot/stdout) & + + local commit_id=`git_show_branch_head $testroot/repo-clone newbranch` + + got send -d newbranch -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 nc -l + + HOSTNAME=`hostname` + printf "HELO localhost\r\n" > $testroot/stdout.expected + printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \ + >> $testroot/stdout.expected + printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected + printf "DATA\r\n" >> $testroot/stdout.expected + printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" >> $testroot/stdout.expected + printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected + printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected + printf "${GOTD_DEVUSER} removed refs/heads/newbranch\r\n" \ + >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + printf "Removed refs/heads/newbranch: $commit_id\n" \ + >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + printf ".\r\n" >> $testroot/stdout.expected + printf "QUIT\r\n" >> $testroot/stdout.expected + + grep -v ^Date $testroot/stdout > $testroot/stdout.filtered + cmp -s $testroot/stdout.expected $testroot/stdout.filtered + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout.filtered + test_done "$testroot" "$ret" + return 1 + fi + + test_done "$testroot" "$ret" +} + +test_tag_created() { + local testroot=`test_init tag_created 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 tag -r $testroot/repo-clone -m "new tag" 1.0 > /dev/null + local commit_id=`git_show_head $testroot/repo-clone` + local tagger_time=`git_show_tagger_time $testroot/repo-clone 1.0` + + (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \ + | timeout 5 nc -l "$GOTD_TEST_SMTP_PORT" > $testroot/stdout) & + + got send -t 1.0 -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 nc -l + + HOSTNAME=`hostname` + printf "HELO localhost\r\n" > $testroot/stdout.expected + printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \ + >> $testroot/stdout.expected + printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected + printf "DATA\r\n" >> $testroot/stdout.expected + printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" >> $testroot/stdout.expected + printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected + printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected + printf "${GOTD_DEVUSER} created refs/tags/1.0\r\n" \ + >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + printf "tag refs/tags/1.0\n" >> $testroot/stdout.expected + printf "from: $GOT_AUTHOR\n" >> $testroot/stdout.expected + d=`date -u -r $tagger_time +"%a %b %e %X %Y UTC"` + printf "date: $d\n" >> $testroot/stdout.expected + printf "object: commit $commit_id\n" >> $testroot/stdout.expected + printf " \n" >> $testroot/stdout.expected + printf " new tag\n \n" >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + printf ".\r\n" >> $testroot/stdout.expected + printf "QUIT\r\n" >> $testroot/stdout.expected + + grep -v ^Date $testroot/stdout > $testroot/stdout.filtered + cmp -s $testroot/stdout.expected $testroot/stdout.filtered + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout.filtered + test_done "$testroot" "$ret" + return 1 + fi + + test_done "$testroot" "$ret" +} + +test_tag_changed() { + local testroot=`test_init tag_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` + + got ref -r $testroot/repo-clone -d refs/tags/1.0 >/dev/null + got tag -r $testroot/repo-clone -m "new tag" 1.0 > /dev/null + local tagger_time=`git_show_tagger_time $testroot/repo-clone 1.0` + + (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \ + | timeout 5 nc -l "$GOTD_TEST_SMTP_PORT" > $testroot/stdout) & + + got send -f -t 1.0 -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 nc -l + + HOSTNAME=`hostname` + printf "HELO localhost\r\n" > $testroot/stdout.expected + printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \ + >> $testroot/stdout.expected + printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected + printf "DATA\r\n" >> $testroot/stdout.expected + printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" >> $testroot/stdout.expected + printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected + printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected + printf "${GOTD_DEVUSER} changed refs/tags/1.0\r\n" \ + >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + printf "tag refs/tags/1.0\n" >> $testroot/stdout.expected + printf "from: $GOT_AUTHOR\n" >> $testroot/stdout.expected + d=`date -u -r $tagger_time +"%a %b %e %X %Y UTC"` + printf "date: $d\n" >> $testroot/stdout.expected + printf "object: commit $commit_id\n" >> $testroot/stdout.expected + printf " \n" >> $testroot/stdout.expected + printf " new tag\n \n" >> $testroot/stdout.expected + printf "\r\n" >> $testroot/stdout.expected + printf ".\r\n" >> $testroot/stdout.expected + printf "QUIT\r\n" >> $testroot/stdout.expected + + grep -v ^Date $testroot/stdout > $testroot/stdout.filtered + cmp -s $testroot/stdout.expected $testroot/stdout.filtered + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout.filtered + 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 +run_test test_branch_created +run_test test_branch_removed +run_test test_tag_created +run_test test_tag_changed