Commit Diff


commit - 2668ec1c3f803454fa75fdcfd86d3a8e6d9ab069
commit + 46fa6498d5de78cc0dce21a09958993f0ebc274d
blob - 68b3cdf4b5d8d53e812ddef4196adf3ae0ebbf34
blob + fd4db424121e7744a2b14792f89c76873bfde529
--- gotsys/gotsys.conf.5
+++ gotsys/gotsys.conf.5
@@ -376,128 +376,242 @@ 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
-.\"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
-.\"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:
-.\".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 Ic to Ar recipient Oo Ic reply to Ar responder 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.
-.\".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.
-.\".It Ic url Ar URL Oo Ic user Ar user Ic password Ar password Oo Ic insecure Oc Oc Oo Ic hmac Ar secret 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.
-.\"Unless the
-.\".Ic insecure
-.\"option is specified the notification target
-.\".Ar URL
-.\"must be a
-.\".Dq https://
-.\"URL to avoid leaking of authentication credentials.
-.\".Pp
-.\"If a
-.\".Ic hmac
-.\".Ar secret
-.\"is provided, the request body will be signed using HMAC, allowing the
-.\"receiver to verify the notification message's authenticity and integrity.
-.\"The signature uses HMAC-SHA256 and will be sent in the HTTP header
-.\".Dq X-Gotd-Signature .
-.\"Suitable secrets can be generated with
-.\".Xr openssl 1
-.\"as follows:
-.\".Pp
-.\".Dl $ openssl rand -base64 32
-.\".Pp
-.\"The request body contains a JSON object with a
-.\".Dq notifications
-.\"property containing an array of notification objects.
-.\"This JSON format is documented in
-.\".Xr gotd 8 .
-.\".El
+.It Ic notify Brq Ar ...
+The
+.Ic notify
+directive enables notifications about new commits or tags
+added to the repository.
+.Pp
+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
+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:
+.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 Ic to Ar recipient Oo Ic reply to Ar responder 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 address that accepts mail.
+.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, for example.
+.It Ic url Ar URL Oo Ic user Ar user Ic password Ar password Oo Ic insecure Oc Oc Oo Ic hmac Ar secret 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.
+Unless the
+.Ic insecure
+option is specified the notification target
+.Ar URL
+must be a
+.Dq https://
+URL to avoid leaking of authentication credentials.
+.Pp
+If a
+.Ic hmac
+.Ar secret
+is provided, the request body will be signed using HMAC, allowing the
+receiver to verify the notification message's authenticity and integrity.
+The signature uses HMAC-SHA256 and will be sent in the HTTP header
+.Dq X-Gotd-Signature .
+Suitable secrets can be generated with
+.Xr openssl 1
+as follows:
+.Pp
+.Dl $ openssl rand -base64 32
+.Pp
+The request body contains a JSON object with a
+.Dq notifications
+property containing an array of notification objects.
+The following notification object properties are always present:
+.Bl -tag -width authenticated_user
+.It Dv repo
+The repository name as a string.
+.It Dv authenticated_user
+The committer's user account as authenticated by
+.Xr gotd 8
+as a string.
+.It Dv type
+The notification object type as a string.
 .El
+.Pp
+Each notification object carries additional type-specific properties.
+The types and their type-specific properties are:
+.Bl -tag -width Ds
+.It Dv commit
+The commit notification object has the following fields.
+Except where noted, all are optional.
+.Bl -tag -width Ds
+.It Dv short
+Boolean, indicates whether the object has all the fields set.
+When several commits are batched in a single send operation, not all of
+the fields are available for each commit object.
+.It Dv id
+The commit ID as string, may be abbreviated.
+.It Dv committer
+An object with the committer information with the following fields:
+.Pp
+.Bl -tag -compact -width Ds
+.It Dv full
+Committer's full name.
+.It Dv name
+Committer's name.
+.It Dv mail
+Committer's mail address.
+.It Dv user
+Committer's username.
+This is the only field guaranteed to be set.
+.El
+.It Dv author
+An object with the author information.
+Has the same fields as the
+.Sq committer
+but may be unset.
+.It Dv date
+Number, representing the number of seconds since the Epoch in UTC.
+.It Dv short_message
+The first line of the commit message.
+This field is always set.
+.It Dv message
+The complete commit message, may be unset.
+.It Dv diffstat
+An object with the summarized changes, may be unset.
+Contains a
+.Sq files
+field with an array of objects describing the changes per-file and a
+.Sq total
+field with the cumulative changes.
+The changes per-file contains the following fields:
+.Pp
+.Bl -tag -compact -width removed
+.It Dv action
+A string describing the action, can be
+.Dq added ,
+.Dq deleted ,
+.Dq modified ,
+.Dq mode changed ,
+or
+.Dq unknown .
+.It Dv file
+The file path.
+.It Dv added
+The number of lines added.
+.It Dv removed
+The number of lines removed.
+.El
+.Pp
+The
+.Sq total
+object contains two fields:
+.Sq added
+and
+.Sq removed
+which are the number of added and removed lines respectively.
+.El
+.It Dv branch-deleted
+The branch deleted notifications has the following fields, all guaranteed
+to be set:
+.Bl -tag -width Ds
+.It Dv ref
+The removed branch reference.
+.It Dv id
+The hash of the commit pointed by the deleted branch.
+.El
+.It Dv tag
+The tag notification has the following fields, all guaranteed to be set:
+.Bl -tag -width Ds
+.It tag
+The tag reference.
+.It tagger
+The user information, with the same format of the
+.Sq committer
+field for the
+.Sq commit
+notification but with all the field guaranteed to be set.
+.It Dv date
+Number, representing the number of seconds since the Epoch in UTC.
+.It Dv object
+The object being tagged.
+It contains the fields
+.Sq type
+with the object type and
+.Sq id
+with the object id being tagged.
+.It Dv message
+The tag message.
+.El
+.El
+.El
+.El
 .Sh EXAMPLES
 .Bd -literal -offset indent
 group developers
@@ -531,12 +645,12 @@ repository "openbsd/ports" {
 		branch "main"
 		tag namespace "refs/tags/"
 	}
-.\"
-.\"	notify {
-.\"		branch "main"
-.\"		reference namespace "refs/tags/"
-.\"		email to openbsd-ports-changes@example.com
-.\"	}
+
+	notify {
+		branch "main"
+		reference namespace "refs/tags/"
+		email to openbsd-ports-changes@example.com
+	}
 }
 
 repository "secret" {
blob - d6c4206f6fde168542a00fd487891e6fa296621a
blob + ba7ff338e3c04fc7b3d068b9db8f76aa2af69e17
--- gotsys/gotsys.h
+++ gotsys/gotsys.h
@@ -105,7 +105,9 @@ struct gotsys_repo {
 	size_t nprotected_branches;
 
 	struct got_pathlist_head notification_refs;
+	size_t num_notification_refs;
 	struct got_pathlist_head notification_ref_namespaces;
+	size_t num_notification_ref_namespaces;
 	struct gotsys_notification_targets notification_targets;
 };
 TAILQ_HEAD(gotsys_repolist, gotsys_repo);
blob - b39d9501ba290cd72e72747e8f31970ce5e09ec4
blob + 97555fb391c138b895b056ac33aa9ae9bf247c3d
--- gotsys/parse.y
+++ gotsys/parse.y
@@ -1255,6 +1255,8 @@ conf_notify_branch(struct gotsys_repo *repo, char *bra
 	}
 	if (pe == NULL)
 		free(refname);
+	else
+		repo->num_notification_refs++;
 
 	return 0;
 }
@@ -1284,11 +1286,70 @@ conf_notify_ref_namespace(struct gotsys_repo *repo, ch
 	}
 	if (pe == NULL)
 		free(s);
+	else
+		repo->num_notification_ref_namespaces++;
 
 	return 0;
 }
 
 static int
+email_address_is_valid(const char *s)
+{
+	const char allowed[] = {
+	    '!', '%', '&', '*', '+', '-', '/', '?',
+	    '^', '_', '`', '.', '{', '|', '}', '~'
+	};
+	size_t i, j, len = strlen(s);
+	char *at;
+	ptrdiff_t local_len;
+	size_t domain_len;
+
+	if (s[0] == '\0' || s[0] == '.')
+		return 0;
+
+	at = strchr(s, '@');
+	if (at == NULL)
+		return 0;
+
+	local_len = at - s;
+	if (local_len == 0 || local_len > 64)
+		return 0;
+	
+	for (i = 0; i < local_len; i++) {
+		if (isalnum(s[i]))
+			continue;
+
+		for (j = 0; j < nitems(allowed); j++) {
+			if (s[i] == allowed[j])
+				break;
+		}
+		if (j < nitems(allowed))
+			continue;
+
+		return 0;
+	}
+
+	if (s[local_len - 1] == '.')
+		return 0;
+
+	if (s[local_len + 1] == '-' || s[len - 1] == '-')
+		return 0;
+
+	domain_len = len - local_len;
+	if (domain_len == 0 || domain_len > 255)
+		return 0;
+
+	for (i = local_len + 1; i < domain_len; i++) {
+		if (isalnum(s[i]) || s[i] == '.' || s[i] == '-')
+			continue;
+
+		return 0;
+	}
+
+	return 1;
+}
+
+static int
 conf_notify_email(struct gotsys_repo *repo, char *sender, char *recipient,
     char *responder, char *hostname, char *port)
 {
@@ -1311,18 +1372,31 @@ conf_notify_email(struct gotsys_repo *repo, char *send
 	}
 	target->type = GOTSYS_NOTIFICATION_VIA_EMAIL;
 	if (sender) {
+		if (!email_address_is_valid(sender)) {
+			yyerror("invalid email address: %s", sender);
+			goto free_target;
+		}
 		target->conf.email.sender = strdup(sender);
 		if (target->conf.email.sender == NULL) {
 			yyerror("strdup: %s", strerror(errno));
 			goto free_target;
 		}
 	}
+
+	if (!email_address_is_valid(recipient)) {
+		yyerror("invalid email address: %s", recipient);
+		goto free_target;
+	}
 	target->conf.email.recipient = strdup(recipient);
 	if (target->conf.email.recipient == NULL) {
 		yyerror("strdup: %s", strerror(errno));
 		goto free_target;
 	}
 	if (responder) {
+		if (!email_address_is_valid(responder)) {
+			yyerror("invalid email address: %s", responder);
+			goto free_target;
+		}
 		target->conf.email.responder = strdup(responder);
 		if (target->conf.email.responder == NULL) {
 			yyerror("strdup: %s", strerror(errno));
@@ -1352,18 +1426,97 @@ free_target:
 	return -1;
 }
 
+static inline int
+should_urlencode(int c)
+{
+	if (c <= ' ' || c >= 127)
+		return 1;
+
+	switch (c) {
+		/* gen-delim */
+	case ':':
+	case '/':
+	case '?':
+	case '#':
+	case '[':
+	case ']':
+	case '@':
+		/* sub-delims */
+	case '!':
+	case '$':
+	case '&':
+	case '\'':
+	case '(':
+	case ')':
+	case '*':
+	case '+':
+	case ',':
+	case ';':
+	case '=':
+		/* needed because the URLs are embedded into gotd.conf */
+	case '\"':
+		return 1;
+	default:
+		return 0;
+	}
+}
+
+static char *
+urlencode(const char *str)
+{
+	const char *s;
+	char *escaped;
+	size_t i, len;
+	int a, b;
+
+	len = 0;
+	for (s = str; *s; ++s) {
+		len++;
+		if (len == 1 && *s == '/')
+			continue;
+		if (should_urlencode(*s))
+			len += 2;
+	}
+
+	escaped = calloc(1, len + 1);
+	if (escaped == NULL)
+		return NULL;
+
+	i = 0;
+	for (s = str; *s; ++s) {
+		if (i == 0 && *s == '/') {
+			escaped[i++] = *s;
+			continue;
+		}
+		if (should_urlencode(*s)) {
+			a = (*s & 0xF0) >> 4;
+			b = (*s & 0x0F);
+
+			escaped[i++] = '%';
+			escaped[i++] = a <= 9 ? ('0' + a) : ('7' + a);
+			escaped[i++] = b <= 9 ? ('0' + b) : ('7' + b);
+		} else
+			escaped[i++] = *s;
+	}
+
+	return escaped;
+}
+
 static const struct got_error *
 parse_url(char **proto, char **host, char **port,
     char **request_path, const char *url)
 {
 	const struct got_error *err = NULL;
 	char *s, *p, *q;
+	size_t i, host_len;
 
 	*proto = *host = *port = *request_path = NULL;
 
 	p = strstr(url, "://");
-	if (!p)
-		return got_error(GOT_ERR_PARSE_URI);
+	if (!p) {
+		return got_error_msg(GOT_ERR_PARSE_URI,
+		    "no protocol specified");
+	}
 
 	*proto = strndup(url, p - url);
 	if (*proto == NULL) {
@@ -1373,10 +1526,8 @@ parse_url(char **proto, char **host, char **port,
 	s = p + 3;
 
 	p = strstr(s, "/");
-	if (p == NULL) {
-		err = got_error(GOT_ERR_PARSE_URI);
-		goto done;
-	}
+	if (p == NULL)
+		p = (char *)&url[strlen(url) - 1];
 
 	q = memchr(s, ':', p - s);
 	if (q) {
@@ -1398,6 +1549,17 @@ parse_url(char **proto, char **host, char **port,
 			err = got_error(GOT_ERR_PARSE_URI);
 			goto done;
 		}
+		if (strcmp(*port, "http") != 0 &&
+		    strcmp(*port, "https") != 0) {
+			const char *errstr;
+
+			(void)strtonum(*port, 1, USHRT_MAX, &errstr);
+			if (errstr != NULL) {
+				err = got_error_fmt(GOT_ERR_PARSE_URI,
+				    "port number '%s' is %s", *port, errstr);
+				goto done;
+			}
+		}
 	} else {
 		*host = strndup(s, p - s);
 		if (*host == NULL) {
@@ -1405,22 +1567,35 @@ parse_url(char **proto, char **host, char **port,
 			goto done;
 		}
 		if ((*host)[0] == '\0') {
-			err = got_error(GOT_ERR_PARSE_URI);
+			err = got_error_msg(GOT_ERR_PARSE_URI,
+			    "hostname cannot be empty");
 			goto done;
 		}
 	}
 
+	host_len = strlen(*host);
+	for (i = 0; i < host_len; i++) {
+		if (isalnum((*host)[i]) ||
+		    (*host)[i] == '.' || (*host)[i] == '-')
+			continue;
+		err = got_error_fmt(GOT_ERR_PARSE_URI,
+		    "invalid hostname: %s", *host);
+		goto done;
+
+	}
+
 	while (p[0] == '/' && p[1] == '/')
 		p++;
-	*request_path = strdup(p);
-	if (*request_path == NULL) {
-		err = got_error_from_errno("strdup");
-		goto done;
+	if (p[0] == '\0') {
+		*request_path = strdup("/");
+		if (*request_path == NULL) {
+			err = got_error_from_errno("strdup");
+		}
+	} else {
+		*request_path = urlencode(p);
+		if (*request_path == NULL)
+			err = got_error_from_errno("calloc");
 	}
-	if ((*request_path)[0] == '\0') {
-		err = got_error(GOT_ERR_PARSE_URI);
-		goto done;
-	}
 done:
 	if (err) {
 		free(*proto);
@@ -1436,6 +1611,68 @@ done:
 }
 
 static int
+basic_auth_user_is_valid(const char *s)
+{
+	size_t i, len;
+
+	if (s[0] == '\0')
+		return 0;
+
+	len = strlen(s);
+	for (i = 0; i < len; i++) {
+		if (s[i] & 0x80)
+			return 0;
+
+		if (isalnum(s[i]) ||
+		    (i > 0 && s[i] == '-') ||
+		    (i > 0 && s[i] == '_') ||
+		    (i > 0 && s[i] == '.'))
+			continue;
+
+		return 0;
+	}
+
+	return 1;
+}
+
+static int
+basic_auth_password_is_valid(const char *s)
+{
+	size_t i, len;
+
+	if (s[0] == '\0')
+		return 0;
+
+	len = strlen(s);
+	for (i = 0; i < len; i++) {
+		if (s[i] & 0x80)
+			return 0;
+		if (iscntrl(s[i]))
+			return 0;
+		if (s[i] == '"')
+			return 0;
+
+	}
+
+	return 1;
+}
+
+static int
+validate_hmac_secret(const char *s, size_t len)
+{
+	static const u_int8_t base64chars[] =
+	    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+	size_t i;
+
+	for (i = 0; i < len; i++) {
+		if (strchr(base64chars, s[i]) == NULL)
+			return 0;
+	}
+
+	return 1;
+}
+
+static int
 conf_notify_http(struct gotsys_repo *repo, char *url, char *user,
     char *password, int insecure, char *hmac_secret)
 {
@@ -1513,11 +1750,36 @@ conf_notify_http(struct gotsys_repo *repo, char *url, 
 	path = NULL;
 
 	if (user) {
+		if (user[0] == '\0') {
+			yyerror("%s: basic auth user names cannot be empty",
+			    url);
+			goto done;
+		}
+		if (!basic_auth_user_is_valid(user)) {
+			yyerror("%s: basic auth user names may only "
+			    "contain alphabetic ASCII characters,  "
+			    "non-leading digits, non-leading hyphens, "
+			    "non-leading underscores, or non-leading "
+			    "periods", url);
+			goto done;
+		}
 		target->conf.http.user = strdup(user);
 		if (target->conf.http.user == NULL) {
 			yyerror("strdup: %s", strerror(errno));
 			goto done;
 		}
+		if (password[0] == '\0') {
+			yyerror("%s: basic auth passwords cannot be empty",
+			    url);
+			goto done;
+		}
+		if (!basic_auth_password_is_valid(user)) {
+			yyerror("%s: passwords for basic auth may only "
+			    "contain ASCII characters, excluding control "
+			    "characters and the \" (double quote) character",
+			    url);
+			goto done;
+		}
 		target->conf.http.password = strdup(password);
 		if (target->conf.http.password == NULL) {
 			yyerror("strdup: %s", strerror(errno));
@@ -1526,6 +1788,16 @@ conf_notify_http(struct gotsys_repo *repo, char *url, 
  	}
 
 	if (hmac_secret) {
+		if (hmac_secret[9] == '\0') {
+			yyerror("hmac secrets cannot be empty");
+			goto done;
+		}
+		if (!validate_hmac_secret(hmac_secret, strlen(hmac_secret))) {
+			yyerror("hmac secrets must be base64-encoded; use "
+			    "'openssl rand -base64 32' output instead of: %s",
+			    hmac_secret);
+			goto done;
+		}
 		target->conf.http.hmac_secret = strdup(hmac_secret);
 		if (target->conf.http.hmac_secret == NULL) {
 			yyerror("strdup: %s", strerror(errno));
blob - f732d4ca5c76b8dbe25a819d920f14dfc615b1cb
blob + 200118a1594f49d882d2f0fb2c642bac8efd8d55
--- gotsysd/gotsysd.h
+++ gotsysd/gotsysd.h
@@ -31,6 +31,10 @@
 #define GOTD_CONF_PATH	"/etc/gotd.conf"
 #endif
 
+#ifndef GOTD_SECRETS_PATH
+#define GOTD_SECRETS_PATH "/etc/gotd-secrets.conf"
+#endif
+
 #ifndef GOTSYSD_PATH_GOTSH
 #define GOTSYSD_PATH_GOTSH		"/usr/local/bin/gotsh"
 #endif
@@ -190,6 +194,15 @@ enum gotsysd_imsg_type {
 	GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES,
 	GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES_ELEM,
 	GOTSYSD_IMSG_SYSCONF_PROTECTED_REFS_DONE,
+	GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS,
+	GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_ELEM,
+	GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_DONE,
+	GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES,
+	GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_ELEM,
+	GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_DONE,
+	GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_EMAIL,
+	GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_HTTP,
+	GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGETS_DONE,
 	GOTSYSD_IMSG_SYSCONF_PARSE_DONE,
 
 	/* Addition of users and groups. */
@@ -436,8 +449,40 @@ struct gotsysd_imsg_pathlist_elem {
 
 	/* Followed by path_len bytes. */
 	/* Followed by data_len bytes. */
+};
+
+/* Structure for GOTSYSD_IMSG_NOTIFICATION_TARGET_EMAIL. */
+struct gotsysd_imsg_notitfication_target_email {
+	size_t sender_len;
+	size_t recipient_len;
+	size_t responder_len;
+	size_t hostname_len;
+	size_t port_len;
+	size_t repo_name_len;
+
+	/*
+	 * Followed by sender_len + responder_len + responder_len +
+	 * hostname_len + port_len + repo_name_len bytes.
+	 */
 };
 
+/* Structure for GOTD_IMSG_NOTIFICATION_TARGET_HTTP. */
+struct gotsysd_imsg_notitfication_target_http {
+	int tls;
+	size_t hostname_len;
+	size_t port_len;
+	size_t path_len;
+	size_t user_len;
+	size_t password_len;
+	size_t hmac_len;
+	size_t repo_name_len;
+
+	/*
+	 * Followed by hostname_len + port_len + path_len + user_len + password_len +
+	 * hmac_len + repo_name_len bytes.
+	 */
+};
+
 #ifndef GOT_LIBEXECDIR
 #define GOT_LIBEXECDIR /usr/libexec
 #endif
@@ -531,6 +576,7 @@ struct gotsys_authorized_keys_list;
 struct gotsys_repolist;
 struct gotsys_repo;
 struct gotsys_access_rule;
+struct gotsys_notification_target;
 struct got_pathlist_head;
 
 const struct got_error *gotsys_imsg_send_users(struct gotsysd_imsgev *,
@@ -561,6 +607,10 @@ const struct got_error *gotsys_imsg_recv_access_rule(
 const struct got_error *gotsys_imsg_recv_pathlist(size_t *, struct imsg *);
 const struct got_error *gotsys_imsg_recv_pathlist_elem(struct imsg *,
     struct got_pathlist_head *);
+const struct got_error *gotsys_imsg_recv_notification_target_email(char **,
+    struct gotsys_notification_target **, struct imsg *);
+const struct got_error *gotsys_imsg_recv_notification_target_http(char **,
+    struct gotsys_notification_target **, struct imsg *);
 
 struct gotsys_uidset_element;
 struct gotsys_uidset;
blob - 7e3a2d02aa64a6ad196c47fb3ce747ed6294522b
blob + 92e28da9546ddf1755fbec09972f0a3fa16394b8
--- gotsysd/helpers.c
+++ gotsysd/helpers.c
@@ -588,6 +588,15 @@ dispatch_helper_child(int fd, short event, void *arg)
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES:
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES_ELEM:
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_REFS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_EMAIL:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_HTTP:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGETS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_PARSE_DONE:
 			if (proc->type != GOTSYSD_IMSG_START_PROG_READ_CONF) {
 				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
@@ -1026,6 +1035,15 @@ helpers_dispatch_sysconf(int fd, short event, void *ar
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES:
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES_ELEM:
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_REFS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_EMAIL:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_HTTP:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGETS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_REPOS_DONE:
 			proc = find_proc(GOTSYSD_IMSG_START_PROG_WRITE_CONF,
 			    1);
blob - de3b5c530209dfd7fd4ee70ed891e96ad029fc17
blob + 12ca1fecf1f999c56f4cc86fe5638973e2a976e2
--- gotsysd/libexec/gotsys-write-conf/gotsys-write-conf.c
+++ gotsysd/libexec/gotsys-write-conf/gotsys-write-conf.c
@@ -49,7 +49,13 @@ static size_t nprotected_refs_needed;
 static size_t nprotected_refs_received;
 static int gotd_conf_tmpfd = -1;
 static char *gotd_conf_tmppath;
+static int gotd_secrets_tmpfd = -1;
+static char *gotd_secrets_tmppath;
 static struct gotsys_access_rule_list global_repo_access_rules;
+static struct got_pathlist_head *notif_refs_cur;
+static size_t *num_notif_refs_cur;
+static size_t num_notif_refs_needed;
+static size_t num_notif_refs_received;
 
 enum writeconf_state {
 	WRITECONF_STATE_EXPECT_USERS,
@@ -298,6 +304,206 @@ write_protected_refs(struct gotsys_repo *repo)
 	}
 
 	RB_FOREACH(pe, got_pathlist_head, &repo->protected_branches) {
+		err = refname_is_valid(pe->path);
+		if (err)
+			return err;
+		ret = dprintf(gotd_conf_tmpfd, "\t\tbranch \"%s\"\n", pe->path);
+		if (ret == -1) {
+			return got_error_from_errno2("dprintf",
+			    gotd_conf_tmppath);
+		}
+		if (ret != 12 + strlen(pe->path))
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    gotd_conf_tmppath);
+	}
+
+	ret = dprintf(gotd_conf_tmpfd, "\t%s\n", closing);
+	if (ret == -1)
+		return got_error_from_errno2("dprintf", gotd_conf_tmppath);
+	if (ret != 2 + strlen(closing))
+		return got_error_fmt(GOT_ERR_IO, "short write to %s",
+		    gotd_conf_tmppath);
+done:
+	free(namespace);
+	return NULL;
+}
+
+static const struct got_error *
+write_notification_target_email(struct gotsys_notification_target *target)
+{
+	char sender[128];
+	char recipient[128];
+	char responder[128];
+	int ret = 0;
+
+	if (target->conf.email.sender) {
+		ret = snprintf(sender, sizeof(sender), " from \"%s\"",
+		    target->conf.email.sender);
+		if (ret == -1)
+			return got_error_from_errno("snprintf");
+		if ((size_t)ret >= sizeof(sender)) {
+			return got_error_msg(GOT_ERR_NO_SPACE,
+			    "notification email sender too long");
+		}
+	} else
+		sender[0] = '\0';
+
+	ret = snprintf(recipient, sizeof(recipient), " to \"%s\"",
+	    target->conf.email.recipient);
+	if (ret == -1)
+		return got_error_from_errno("snprintf");
+	if ((size_t)ret >= sizeof(recipient)) {
+		return got_error_msg(GOT_ERR_NO_SPACE,
+		    "notification email recipient too long");
+	}
+
+	if (target->conf.email.responder) {
+		ret = snprintf(responder, sizeof(responder), " reply to \"%s\"",
+		    target->conf.email.responder);
+		if (ret == -1)
+			return got_error_from_errno("snprintf");
+		if ((size_t)ret >= sizeof(responder)) {
+			return got_error_msg(GOT_ERR_NO_SPACE,
+			    "notification email responder too long");
+		}
+	} else
+		responder[0] = '\0';
+
+	ret = dprintf(gotd_conf_tmpfd, "\t\temail%s%s%s\n",
+	    sender, recipient, responder);
+	if (ret == -1)
+		return got_error_from_errno2("dprintf", gotd_conf_tmppath);
+	if (ret != 8 + strlen(sender) + strlen(recipient) + strlen(responder)) {
+		return got_error_fmt(GOT_ERR_IO, "short write to %s",
+		    gotd_conf_tmppath);
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+write_notification_target_http(struct gotsys_notification_target *target,
+    int idx)
+{
+	char proto[16];
+	char port[16];
+	char label[16];
+	char auth[128];
+	char insecure[16];
+	char hmac[128];
+	int ret = 0;
+
+	insecure[0] = '\0';
+
+	if (target->conf.http.tls) {
+		if (strlcpy(proto, "https://", sizeof(proto)) >=
+		    sizeof(proto)) {
+			return got_error_msg(GOT_ERR_NO_SPACE,
+			    "http notification protocol too long");
+		}
+	} else {
+		if (strlcpy(proto, "http://", sizeof(proto)) >=
+		    sizeof(proto)) {
+			return got_error_msg(GOT_ERR_NO_SPACE,
+			    "http notification protocol too long");
+		}
+
+		if (target->conf.http.user && target->conf.http.password) {
+			if (strlcpy(insecure, " insecure", sizeof(insecure)) >=
+			    sizeof(insecure)) {
+				return got_error_msg(GOT_ERR_NO_SPACE, "http "
+				    "notification insecure keyword too long");
+			}
+		}
+	}
+
+	if (target->conf.http.port) {
+		ret = snprintf(port, sizeof(port), ":%s",
+		    target->conf.http.port);
+		if (ret == -1)
+			return got_error_from_errno("snprintf");
+		if ((size_t)ret >= sizeof(port)) {
+			return got_error_msg(GOT_ERR_NO_SPACE,
+			    "notification http port too long");
+		}
+	} else
+		port[0] = '\0';
+
+	if (target->conf.http.user && target->conf.http.password) {
+		ret = snprintf(label, sizeof(label), "basic%d", idx);
+		if (ret == -1)
+			return got_error_from_errno("snprintf");
+		if ((size_t)ret >= sizeof(label)) {
+			return got_error_msg(GOT_ERR_NO_SPACE,
+			    "basic auth label too long");
+		}
+
+		ret = snprintf(auth, sizeof(auth), " auth %s", label);
+		if (ret == -1)
+			return got_error_from_errno("snprintf");
+		if ((size_t)ret >= sizeof(label)) {
+			return got_error_msg(GOT_ERR_NO_SPACE,
+			    "http notification auth too long");
+		}
+	} else
+		auth[0] = '\0';
+
+	if (target->conf.http.hmac_secret) {
+		ret = snprintf(label, sizeof(label), "hmac%d", idx);
+		if (ret == -1)
+			return got_error_from_errno("snprintf");
+		if ((size_t)ret >= sizeof(label)) {
+			return got_error_msg(GOT_ERR_NO_SPACE,
+			    "notification http hmac label too long");
+		}
+
+		ret = snprintf(hmac, sizeof(hmac), " hmac %s", label);
+		if (ret == -1)
+			return got_error_from_errno("snprintf");
+		if ((size_t)ret >= sizeof(label)) {
+			return got_error_msg(GOT_ERR_NO_SPACE,
+			    "http notification hmac too long");
+		}
+	} else
+		hmac[0] = '\0';
+
+	ret = dprintf(gotd_conf_tmpfd, "\t\turl \"%s%s%s/%s\"%s%s%s\n",
+		proto, target->conf.http.hostname, port,
+		target->conf.http.path, auth, insecure, hmac);
+	if (ret == -1)
+		return got_error_from_errno2("dprintf", gotd_conf_tmppath);
+	if (ret != 10 + strlen(proto) + strlen(target->conf.http.hostname) +
+	    strlen(port) + strlen(target->conf.http.path) + strlen(auth) +
+	    strlen(insecure) + strlen(hmac)) {
+		return got_error_fmt(GOT_ERR_IO, "short write to %s",
+		    gotd_conf_tmppath);
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+write_notification_targets(struct gotsys_repo *repo)
+{
+	const struct got_error *err = NULL;
+	struct got_pathlist_entry *pe;
+	struct gotsys_notification_target *target;
+	const char *opening = "notify {";
+	const char *closing = "}";
+	char *namespace = NULL;
+	int ret = 0, i;
+
+	if (STAILQ_EMPTY(&repo->notification_targets))
+		return NULL;
+
+	ret = dprintf(gotd_conf_tmpfd, "\t%s\n", opening);
+	if (ret == -1)
+		return got_error_from_errno2("dprintf", gotd_conf_tmppath);
+	if (ret != 2 + strlen(opening))
+		return got_error_fmt(GOT_ERR_IO, "short write to %s",
+		    gotd_conf_tmppath);
+
+	RB_FOREACH(pe, got_pathlist_head, &repo->notification_refs) {
 		err = refname_is_valid(pe->path);
 		if (err)
 			return err;
@@ -311,6 +517,47 @@ write_protected_refs(struct gotsys_repo *repo)
 			    gotd_conf_tmppath);
 	}
 
+	RB_FOREACH(pe, got_pathlist_head, &repo->notification_ref_namespaces) {
+		namespace = strdup(pe->path);
+		if (namespace == NULL)
+			return got_error_from_errno("strdup");
+
+		got_path_strip_trailing_slashes(namespace);
+		err = refname_is_valid(namespace);
+		if (err)
+			goto done;
+
+		ret = dprintf(gotd_conf_tmpfd,
+		    "\t\treference namespace \"%s\"\n", namespace);
+		if (ret == -1) {
+			err = got_error_from_errno2("dprintf",
+			    gotd_conf_tmppath);
+			goto done;
+		}
+		if (ret != 25 + strlen(namespace)) {
+			err = got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    gotd_conf_tmppath);
+			goto done;
+		}
+		free(namespace);
+		namespace = NULL;
+	}
+
+	i = 0;
+	STAILQ_FOREACH(target, &repo->notification_targets, entry) {
+		i++;
+		switch (target->type) {
+		case GOTSYS_NOTIFICATION_VIA_EMAIL:
+			err = write_notification_target_email(target);
+			break;
+		case GOTSYS_NOTIFICATION_VIA_HTTP:
+			err = write_notification_target_http(target, i);
+			break;
+		default:
+			break;
+		}
+	}
+
 	ret = dprintf(gotd_conf_tmpfd, "\t%s\n", closing);
 	if (ret == -1)
 		return got_error_from_errno2("dprintf", gotd_conf_tmppath);
@@ -319,10 +566,112 @@ write_protected_refs(struct gotsys_repo *repo)
 		    gotd_conf_tmppath);
 done:
 	free(namespace);
+	return err;
+}
+
+static const struct got_error *
+write_repo_secrets(off_t *written, struct gotsys_repo *repo)
+{
+	struct gotsys_notification_target *target;
+	char label[32];
+	int ret = 0, i = 0;
+	size_t len;
+
+	STAILQ_FOREACH(target, &repo->notification_targets, entry) {
+		if (target->type != GOTSYS_NOTIFICATION_VIA_HTTP)
+			continue;
+
+		if (target->conf.http.user == NULL &&
+		    target->conf.http.password == NULL &&
+		    target->conf.http.hmac_secret == NULL)
+			continue;
+
+		i++;
+
+		if (target->conf.http.user && target->conf.http.password) {
+			ret = snprintf(label, sizeof(label), "basic%d", i);
+			if (ret == -1)
+				return got_error_from_errno("snprintf");
+			if ((size_t)ret >= sizeof(label)) {
+				return got_error_msg(GOT_ERR_NO_SPACE,
+				    "basic auth label too long");
+			}
+
+			ret = dprintf(gotd_secrets_tmpfd,
+			    "auth %s user \"%s\" password \"%s\"\n", label,
+			    target->conf.http.user, target->conf.http.password);
+			if (ret == -1)
+				return got_error_from_errno2("dprintf",
+				    gotd_secrets_tmppath);
+			len = strlen(label) +
+			    strlen(target->conf.http.user) +
+			    strlen(target->conf.http.password);
+			if (ret != 26 + len) {
+				return got_error_fmt(GOT_ERR_IO,
+				    "short write to %s", gotd_secrets_tmppath);
+			}
+			*written += ret;
+		}
+
+		if (target->conf.http.hmac_secret) {
+			ret = snprintf(label, sizeof(label), "hmac%d", i);
+			if (ret == -1)
+				return got_error_from_errno("snprintf");
+			if ((size_t)ret >= sizeof(label)) {
+				return got_error_msg(GOT_ERR_NO_SPACE,
+				    "hmac secret label too long");
+			}
+			ret = dprintf(gotd_secrets_tmpfd, "hmac %s \"%s\"\n",
+			    label, target->conf.http.hmac_secret);
+			if (ret == -1)
+				return got_error_from_errno2("dprintf",
+				    gotd_secrets_tmppath);
+			len = strlen(label) +
+			    strlen(target->conf.http.hmac_secret);
+			if (ret != 9 + len) {
+				return got_error_fmt(GOT_ERR_IO,
+				    "short write to %s", gotd_secrets_tmppath);
+			}
+			*written += ret;
+		}
+	}
+
 	return NULL;
 }
 
 static const struct got_error *
+prepare_gotd_secrets(void)
+{
+	const struct got_error *err = NULL;
+	struct gotsys_repo *repo;
+	off_t written = 0;
+
+	if (ftruncate(gotd_secrets_tmpfd, 0) == -1)
+		return got_error_from_errno("ftruncate");
+
+	TAILQ_FOREACH(repo, &gotsysconf.repos, entry) {
+		err = write_repo_secrets(&written, repo);
+		if (err)
+			return err;
+	}
+
+	if (written == 0) {
+		if (unlink(gotd_secrets_tmppath) == -1) {
+			return got_error_from_errno2("unlink",
+			    gotd_secrets_tmppath);
+		}
+		free(gotd_secrets_tmppath);
+		gotd_secrets_tmppath = NULL;
+
+		if (close(gotd_secrets_tmpfd) == -1)
+			return got_error_from_errno("close");
+		gotd_secrets_tmpfd = -1;
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
 write_gotd_conf(void)
 {
 	const struct got_error *err = NULL;
@@ -405,6 +754,10 @@ write_gotd_conf(void)
 		if (err)
 			return err;
 
+		err = write_notification_targets(repo);
+		if (err)
+			return err;
+
 		ret = dprintf(gotd_conf_tmpfd, "}\n");
 		if (ret == -1)
 			return got_error_from_errno2("dprintf",
@@ -415,6 +768,21 @@ write_gotd_conf(void)
 		}
 	}
 
+	if (gotd_secrets_tmppath != NULL && gotd_secrets_tmpfd != -1) {
+		if (fchmod(gotd_secrets_tmpfd, 0600) == -1) {
+			return got_error_from_errno_fmt("chmod 0600 %s",
+			    gotd_secrets_tmppath);
+		}
+			
+		if (rename(gotd_secrets_tmppath, GOTD_SECRETS_PATH) == -1) {
+			return got_error_from_errno_fmt("rename %s to %s",
+			    gotd_conf_tmppath, GOTD_SECRETS_PATH);
+		}
+
+		free(gotd_secrets_tmppath);
+		gotd_secrets_tmppath = NULL;
+	}
+
 	if (fchmod(gotd_conf_tmpfd, 0644) == -1) {
 		return got_error_from_errno_fmt("chmod 0644 %s",
 		    gotd_conf_tmppath);
@@ -692,8 +1060,134 @@ dispatch_event(int fd, short event, void *arg)
 				    writeconf_state);
 				break;
 			}
-			repo_cur = NULL;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS:
+			if (repo_cur == NULL ||
+			    notif_refs_cur != NULL ||
+			    num_notif_refs_needed != 0 ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist(&npaths, &imsg);
+			if (err)
+				break;
+			notif_refs_cur = &repo_cur->notification_refs;
+			num_notif_refs_cur = &repo_cur->num_notification_refs;
+			num_notif_refs_needed = npaths;
+			num_notif_refs_received = 0;
 			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES:
+			if (repo_cur == NULL ||
+			    notif_refs_cur != NULL ||
+			    num_notif_refs_needed != 0 ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist(&npaths, &imsg);
+			if (err)
+				break;
+			notif_refs_cur =
+			    &repo_cur->notification_ref_namespaces;
+			num_notif_refs_cur =
+			    &repo_cur->num_notification_ref_namespaces;
+			num_notif_refs_needed = npaths;
+			num_notif_refs_received = 0;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_ELEM:
+			if (notif_refs_cur == NULL ||
+			    num_notif_refs_cur == NULL ||
+			    num_notif_refs_needed == 0 ||
+			    num_notif_refs_received >=
+			    num_notif_refs_needed ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist_elem(&imsg,
+			    notif_refs_cur);
+			if (err)
+				break;
+			if (++num_notif_refs_received >=
+			    num_notif_refs_needed) {
+				notif_refs_cur = NULL;
+				*num_notif_refs_cur = num_notif_refs_received;
+				num_notif_refs_needed = 0;
+				num_notif_refs_received = 0;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_DONE:
+			if (repo_cur == NULL ||
+			    num_notif_refs_needed != 0 ||
+			    notif_refs_cur != NULL ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_DONE:
+			if (repo_cur == NULL ||
+			    num_notif_refs_needed != 0 ||
+			    notif_refs_cur != NULL ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_EMAIL: {
+			struct gotsys_notification_target *target;
+
+			if (repo_cur == NULL ||
+			    num_notif_refs_needed != 0 ||
+			    notif_refs_cur != NULL ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_notification_target_email(NULL,
+			    &target, &imsg);
+			if (err)
+				break;
+			STAILQ_INSERT_TAIL(&repo_cur->notification_targets,
+			    target, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_HTTP: {
+			struct gotsys_notification_target *target;
+
+			if (repo_cur == NULL ||
+			    num_notif_refs_needed != 0 ||
+			    notif_refs_cur != NULL ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_notification_target_http(NULL,
+			    &target, &imsg);
+			if (err)
+				break;
+			STAILQ_INSERT_TAIL(&repo_cur->notification_targets,
+			    target, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGETS_DONE:
+			break;
 		case GOTSYSD_IMSG_SYSCONF_REPOS_DONE:
 			if (writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
 				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
@@ -704,6 +1198,9 @@ dispatch_event(int fd, short event, void *arg)
 			}
 			repo_cur = NULL;
 			writeconf_state = WRITECONF_STATE_WRITE_CONF;
+			err = prepare_gotd_secrets();
+			if (err)
+				break;
 			err = write_gotd_conf();
 			if (err)
 				break;
@@ -774,6 +1271,10 @@ main(int argc, char *argv[])
 	    GOTD_CONF_PATH, "");
 	if (err)
 		goto done;
+	err = got_opentemp_named_fd(&gotd_secrets_tmppath, &gotd_secrets_tmpfd,
+	    GOTD_CONF_PATH, "");
+	if (err)
+		goto done;
 #ifndef PROFILE
 	if (pledge("stdio rpath wpath cpath fattr chown unveil", NULL) == -1) {
 		err = got_error_from_errno("pledge");
@@ -785,11 +1286,21 @@ main(int argc, char *argv[])
 		goto done;
 	}
 
+	if (unveil(gotd_secrets_tmppath, "rwc") == -1) {
+		err = got_error_from_errno2("unveil rwc", gotd_secrets_tmppath);
+		goto done;
+	}
+
 	if (unveil(GOTD_CONF_PATH, "rwc") == -1) {
 		err = got_error_from_errno2("unveil rwc", GOTD_CONF_PATH);
 		goto done;
 	}
 
+	if (unveil(GOTD_SECRETS_PATH, "rwc") == -1) {
+		err = got_error_from_errno2("unveil rwc", GOTD_SECRETS_PATH);
+		goto done;
+	}
+
 	if (unveil(NULL, NULL) == -1) {
 		err = got_error_from_errno("unveil");
 		goto done;
@@ -811,11 +1322,18 @@ done:
 	if (gotd_conf_tmppath && unlink(gotd_conf_tmppath) == -1 && err == NULL)
 		err = got_error_from_errno2("unlink", gotd_conf_tmppath);
 	free(gotd_conf_tmppath);
+	if (gotd_secrets_tmppath && unlink(gotd_secrets_tmppath) == -1 &&
+	    err == NULL)
+		err = got_error_from_errno2("unlink", gotd_secrets_tmppath);
+	free(gotd_secrets_tmppath);
 	if (close(GOTSYSD_FILENO_MSG_PIPE) == -1 && err == NULL)
 		err = got_error_from_errno("close");
 	if (gotd_conf_tmpfd != -1 && close(gotd_conf_tmpfd) == -1 &&
 	    err == NULL)
 		err = got_error_from_errno("close");
+	if (gotd_secrets_tmpfd != -1 && close(gotd_secrets_tmpfd) == -1 &&
+	    err == NULL)
+		err = got_error_from_errno("close");
 	if (err)
 		gotsysd_imsg_send_error(&iev.ibuf, 0, 0, err);
 	imsgbuf_clear(&iev.ibuf);
blob - 8d18a175cf0653c1c2bc59bfdeb57c4fc6a92728
blob + 0c5a12d1a927caa5c343c9f24680f5e65d37be18
--- gotsysd/sysconf.c
+++ gotsysd/sysconf.c
@@ -80,6 +80,10 @@ static struct gotsysd_sysconf {
 	size_t nprotected_refs_needed;
 	size_t nprotected_refs_received;
 	struct gotsys_access_rule_list *global_repo_access_rules;
+	struct got_pathlist_head *notif_refs_cur;
+	size_t *num_notif_refs_cur;
+	size_t num_notif_refs_needed;
+	size_t num_notif_refs_received;
 } gotsysd_sysconf;
 
 static struct gotsys_conf gotsysconf;
@@ -494,8 +498,148 @@ sysconf_dispatch_libexec(int fd, short event, void *ar
 				break;
 			}
 			log_debug("done receiving protected refs");
-			gotsysd_sysconf.repo_cur = NULL;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS:
+			if (gotsysd_sysconf.repo_cur == NULL ||
+			    gotsysd_sysconf.notif_refs_cur != NULL ||
+			    gotsysd_sysconf.num_notif_refs_needed != 0 ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist(&npaths, &imsg);
+			if (err)
+				break;
+			gotsysd_sysconf.notif_refs_cur =
+			    &gotsysd_sysconf.repo_cur->notification_refs;
+			gotsysd_sysconf.num_notif_refs_cur =
+			    &gotsysd_sysconf.repo_cur->num_notification_refs;
+			gotsysd_sysconf.num_notif_refs_needed = npaths;
+			gotsysd_sysconf.num_notif_refs_received = 0;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES:
+			if (gotsysd_sysconf.repo_cur == NULL ||
+			    gotsysd_sysconf.notif_refs_cur != NULL ||
+			    gotsysd_sysconf.num_notif_refs_needed != 0 ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist(&npaths, &imsg);
+			if (err)
+				break;
+			gotsysd_sysconf.notif_refs_cur =
+			    &gotsysd_sysconf.repo_cur->notification_ref_namespaces;
+			gotsysd_sysconf.num_notif_refs_cur =
+			    &gotsysd_sysconf.repo_cur->num_notification_ref_namespaces;
+			gotsysd_sysconf.num_notif_refs_needed = npaths;
+			gotsysd_sysconf.num_notif_refs_received = 0;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_ELEM:
+			if (gotsysd_sysconf.notif_refs_cur == NULL ||
+			    gotsysd_sysconf.num_notif_refs_cur == NULL ||
+			    gotsysd_sysconf.num_notif_refs_needed == 0 ||
+			    gotsysd_sysconf.num_notif_refs_received >=
+			    gotsysd_sysconf.num_notif_refs_needed ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist_elem(&imsg,
+			    gotsysd_sysconf.notif_refs_cur);
+			if (err)
+				break;
+			if (++gotsysd_sysconf.num_notif_refs_received >=
+			    gotsysd_sysconf.num_notif_refs_needed) {
+				gotsysd_sysconf.notif_refs_cur = NULL;
+				*gotsysd_sysconf.num_notif_refs_cur =
+				    gotsysd_sysconf.num_notif_refs_received;
+				gotsysd_sysconf.num_notif_refs_needed = 0;
+				gotsysd_sysconf.num_notif_refs_received = 0;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_DONE:
+			if (gotsysd_sysconf.repo_cur == NULL ||
+			    gotsysd_sysconf.num_notif_refs_needed != 0 ||
+			    gotsysd_sysconf.notif_refs_cur != NULL ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			log_debug("done receiving notification refs");
+			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_DONE:
+			if (gotsysd_sysconf.repo_cur == NULL ||
+			    gotsysd_sysconf.num_notif_refs_needed != 0 ||
+			    gotsysd_sysconf.notif_refs_cur != NULL ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			log_debug("done receiving notification ref namespaces");
+			break;
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_EMAIL: {
+			struct gotsys_notification_target *target;
+
+			if (gotsysd_sysconf.repo_cur == NULL ||
+			    gotsysd_sysconf.num_notif_refs_needed != 0 ||
+			    gotsysd_sysconf.notif_refs_cur != NULL ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_notification_target_email(NULL,
+			    &target, &imsg);
+			if (err)
+				break;
+			STAILQ_INSERT_TAIL(
+			    &gotsysd_sysconf.repo_cur->notification_targets,
+			    target, entry);
 			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_HTTP: {
+			struct gotsys_notification_target *target;
+
+			if (gotsysd_sysconf.repo_cur == NULL ||
+			    gotsysd_sysconf.num_notif_refs_needed != 0 ||
+			    gotsysd_sysconf.notif_refs_cur != NULL ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_notification_target_http(NULL,
+			    &target, &imsg);
+			if (err)
+				break;
+			STAILQ_INSERT_TAIL(
+			    &gotsysd_sysconf.repo_cur->notification_targets,
+			    target, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGETS_DONE:
+			break;
 		case GOTSYSD_IMSG_SYSCONF_REPOS_DONE:
 			if (gotsysd_sysconf.state !=
 			    SYSCONF_STATE_EXPECT_REPOS) {
blob - 58e4ea0fdbf1305b478ed52f32a261460972f9d7
blob + 08719ff3c688ae2778ff81d0668fb2e6efdb4d77
--- lib/gotsys_imsg.c
+++ lib/gotsys_imsg.c
@@ -742,11 +742,264 @@ send_protected_refs(struct gotsysd_imsgev *iev, struct
 	if (gotsysd_imsg_compose_event(iev,
 	    GOTSYSD_IMSG_SYSCONF_PROTECTED_REFS_DONE, 0, -1, NULL, 0) == -1)
 		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	return NULL;
+}
+
+static const struct got_error *
+send_notification_target_email(struct gotsysd_imsgev *iev,
+    const char *repo_name, struct gotsys_notification_target *target)
+{
+	struct gotsysd_imsg_notitfication_target_email itarget;
+	struct ibuf *wbuf = NULL;
+
+	memset(&itarget, 0, sizeof(itarget));
+
+	if (target->conf.email.sender)
+		itarget.sender_len = strlen(target->conf.email.sender);
+	if (target->conf.email.recipient)
+		itarget.recipient_len = strlen(target->conf.email.recipient);
+	if (target->conf.email.responder)
+		itarget.responder_len = strlen(target->conf.email.responder);
+	if (target->conf.email.hostname)
+		itarget.hostname_len = strlen(target->conf.email.hostname);
+	if (target->conf.email.port)
+		itarget.port_len = strlen(target->conf.email.port);
+	itarget.repo_name_len = strlen(repo_name);
+
+	wbuf = imsg_create(&iev->ibuf,
+	    GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_EMAIL,
+	    0, 0, sizeof(itarget) + itarget.sender_len + itarget.recipient_len +
+	    itarget.responder_len + itarget.hostname_len + itarget.port_len +
+	    itarget.repo_name_len);
+	if (wbuf == NULL) {
+		return got_error_from_errno("imsg_create "
+		    "NOTIFICATION_TARGET_EMAIL");
+	}
+
+	if (imsg_add(wbuf, &itarget, sizeof(itarget)) == -1) {
+		return got_error_from_errno("imsg_add "
+		    "NOTIFICATION_TARGET_EMAIL");
+	}
+	if (target->conf.email.sender) {
+		if (imsg_add(wbuf, target->conf.email.sender,
+		    itarget.sender_len) == -1) {
+			return got_error_from_errno("imsg_add "
+			    "NOTIFICATION_TARGET_EMAIL");
+		}
+	}
+
+	if (target->conf.email.recipient) {
+		if (imsg_add(wbuf, target->conf.email.recipient,
+		    itarget.recipient_len) == -1) {
+			return got_error_from_errno("imsg_add "
+			    "NOTIFICATION_TARGET_EMAIL");
+		}
+	}
+	if (target->conf.email.responder) {
+		if (imsg_add(wbuf, target->conf.email.responder,
+		    itarget.responder_len) == -1) {
+			return got_error_from_errno("imsg_add "
+			    "NOTIFICATION_TARGET_EMAIL");
+		}
+	}
+	if (target->conf.email.hostname) {
+		if (imsg_add(wbuf, target->conf.email.hostname,
+		    itarget.hostname_len) == -1) {
+			return got_error_from_errno("imsg_add "
+			    "NOTIFICATION_TARGET_EMAIL");
+		}
+	}
+	if (target->conf.email.port) {
+		if (imsg_add(wbuf, target->conf.email.port,
+		    itarget.port_len) == -1) {
+			return got_error_from_errno("imsg_add "
+			    "NOTIFICATION_TARGET_EMAIL");
+		}
+	}
+	if (imsg_add(wbuf, repo_name, itarget.repo_name_len) == -1) {
+		return got_error_from_errno("imsg_add "
+		    "NOTIFICATION_TARGET_EMAIL");
+	}
 
+	imsg_close(&iev->ibuf, wbuf);
+	gotsysd_imsg_event_add(iev);
 	return NULL;
 }
 
 static const struct got_error *
+send_notification_target_http(struct gotsysd_imsgev *iev, const char *repo_name,
+    struct gotsys_notification_target *target)
+{
+	struct gotsysd_imsg_notitfication_target_http itarget;
+	struct ibuf *wbuf = NULL;
+
+	memset(&itarget, 0, sizeof(itarget));
+
+	itarget.tls = target->conf.http.tls;
+	itarget.hostname_len = strlen(target->conf.http.hostname);
+	itarget.port_len = strlen(target->conf.http.port);
+	itarget.path_len = strlen(target->conf.http.path);
+	if (target->conf.http.user)
+		itarget.user_len = strlen(target->conf.http.user);
+	if (target->conf.http.password)
+		itarget.password_len = strlen(target->conf.http.password);
+	if (target->conf.http.hmac_secret)
+		itarget.hmac_len = strlen(target->conf.http.hmac_secret);
+	itarget.repo_name_len = strlen(repo_name);
+
+	wbuf = imsg_create(&iev->ibuf,
+	    GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_HTTP,
+	    0, 0, sizeof(itarget) + itarget.hostname_len + itarget.port_len +
+	    itarget.path_len + itarget.user_len + itarget.password_len +
+	    itarget.hmac_len + itarget.repo_name_len);
+	if (wbuf == NULL) {
+		return got_error_from_errno("imsg_create "
+		    "NOTIFICATION_TARGET_HTTP");
+	}
+
+	if (imsg_add(wbuf, &itarget, sizeof(itarget)) == -1) {
+		return got_error_from_errno("imsg_add "
+		    "NOTIFICATION_TARGET_HTTP");
+	}
+	if (imsg_add(wbuf, target->conf.http.hostname,
+	    itarget.hostname_len) == -1) {
+		return got_error_from_errno("imsg_add "
+		    "NOTIFICATION_TARGET_HTTP");
+	}
+	if (imsg_add(wbuf, target->conf.http.port,
+	    itarget.port_len) == -1) {
+		return got_error_from_errno("imsg_add "
+		    "NOTIFICATION_TARGET_HTTP");
+	}
+	if (imsg_add(wbuf, target->conf.http.path,
+	    itarget.path_len) == -1) {
+		return got_error_from_errno("imsg_add "
+		    "NOTIFICATION_TARGET_HTTP");
+	}
+
+	if (target->conf.http.user) {
+		if (imsg_add(wbuf, target->conf.http.user, itarget.user_len) == -1)
+			return got_error_from_errno("imsg_add NOTIFICATION_TARGET_HTTP");
+	}
+	if (target->conf.http.password) {
+		if (imsg_add(wbuf, target->conf.http.password,
+		    itarget.password_len) == -1)
+			return got_error_from_errno("imsg_add NOTIFICATION_TARGET_HTTP");
+	}
+	if (target->conf.http.hmac_secret) {
+		if (imsg_add(wbuf, target->conf.http.hmac_secret,
+		    itarget.hmac_len) == -1) {
+			return got_error_from_errno("imsg_add "
+			    "NOTIFICATION_TARGET_HTTP");
+		}
+	}
+	if (imsg_add(wbuf, repo_name, itarget.repo_name_len) == -1) {
+		return got_error_from_errno("imsg_add "
+		    "NOTIFICATION_TARGET_HTTP");
+	}
+
+	imsg_close(&iev->ibuf, wbuf);
+	gotsysd_imsg_event_add(iev);
+	return NULL;
+}
+
+static const struct got_error *
+send_notification_target(struct gotsysd_imsgev *iev, const char *repo_name,
+    struct gotsys_notification_target *target)
+{
+	const struct got_error *err = NULL;
+
+	switch (target->type) {
+	case GOTSYS_NOTIFICATION_VIA_EMAIL:
+		err = send_notification_target_email(iev, repo_name, target);
+		break;
+	case GOTSYS_NOTIFICATION_VIA_HTTP:
+		err = send_notification_target_http(iev, repo_name, target);
+		break;
+	default:
+		break;
+	}
+
+	return err;
+}
+
+static const struct got_error *
+send_notification_targets(struct gotsysd_imsgev *iev, const char *repo_name,
+    struct gotsys_notification_targets *targets)
+{
+	const struct got_error *err = NULL;
+	struct gotsys_notification_target *target;
+
+	STAILQ_FOREACH(target, targets, entry) {
+		err = send_notification_target(iev, repo_name, target);
+		if (err)
+			return err;
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGETS_DONE, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	return NULL;
+}
+
+static const struct got_error *
+send_notification_config(struct gotsysd_imsgev *iev, struct gotsys_repo *repo)
+{
+	const struct got_error *err = NULL;
+	struct got_pathlist_entry *pe;
+	struct gotsysd_imsg_pathlist ilist;
+
+	memset(&ilist, 0, sizeof(ilist));
+
+	ilist.nelem = repo->num_notification_refs;
+	if (ilist.nelem > 0) {
+		if (gotsysd_imsg_compose_event(iev,
+		    GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS, 0, -1,
+		    &ilist, sizeof(ilist)) == -1) {
+			return got_error_from_errno("imsg compose "
+			    "NOTIFICATION_REFS");
+		}
+
+		RB_FOREACH(pe, got_pathlist_head, &repo->notification_refs) {
+			err = send_pathlist_elem(iev, pe->path,
+			    GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_ELEM);
+			if (err)
+				return err;
+		}
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_DONE, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	ilist.nelem = repo->num_notification_ref_namespaces;
+	if (ilist.nelem > 0) {
+		if (gotsysd_imsg_compose_event(iev,
+		    GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES, 0, -1,
+		    &ilist, sizeof(ilist)) == -1) {
+			return got_error_from_errno("imsg compose "
+			    "NOTIFICATION_REF_NAMESPACES");
+		}
+
+		RB_FOREACH(pe, got_pathlist_head,
+		    &repo->notification_ref_namespaces) {
+			err = send_pathlist_elem(iev, pe->path,
+			    GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_ELEM);
+			if (err)
+				return err;
+		}
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_DONE, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	return send_notification_targets(iev, repo->name, &repo->notification_targets);
+}
+
+static const struct got_error *
 send_repo(struct gotsysd_imsgev *iev, struct gotsys_repo *repo)
 {
 	const struct got_error *err;
@@ -784,7 +1037,9 @@ send_repo(struct gotsysd_imsgev *iev, struct gotsys_re
 	if (err)
 		return err;
 
-	/* TODO: send notification config */
+	err = send_notification_config(iev, repo);
+	if (err)
+		return err;
 
 	return NULL;
 }
@@ -957,3 +1212,269 @@ gotsys_imsg_recv_pathlist_elem(struct imsg *imsg,
 		free(path);
 	return err;
 }
+
+const struct got_error *
+gotsys_imsg_recv_notification_target_email(char **repo_name,
+    struct gotsys_notification_target **new_target, struct imsg *imsg)
+{
+	const struct got_error *err = NULL;
+	struct gotsysd_imsg_notitfication_target_email itarget;
+	struct gotsys_notification_target *target;
+	size_t datalen;
+
+	if (repo_name)
+		*repo_name = NULL;
+	*new_target = NULL;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen < sizeof(itarget))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&itarget, imsg->data, sizeof(itarget));
+
+	if (datalen != sizeof(itarget) + itarget.sender_len +
+	    itarget.recipient_len + itarget.responder_len +
+	    itarget.hostname_len + itarget.port_len + itarget.repo_name_len)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	if (itarget.recipient_len == 0 || itarget.repo_name_len == 0)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	target = calloc(1, sizeof(*target));
+	if (target == NULL)
+		return got_error_from_errno("calloc");
+
+	target->type = GOTSYS_NOTIFICATION_VIA_EMAIL;
+
+	if (itarget.sender_len) {
+		target->conf.email.sender = strndup(imsg->data +
+		    sizeof(itarget), itarget.sender_len);
+		if (target->conf.email.sender == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if (strlen(target->conf.email.sender) != itarget.sender_len) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			goto done;
+		}
+	}
+
+	target->conf.email.recipient = strndup(imsg->data + sizeof(itarget) +
+	    itarget.sender_len, itarget.recipient_len);
+	if (target->conf.email.recipient == NULL) {
+		err = got_error_from_errno("strndup");
+		goto done;
+	}
+	if (strlen(target->conf.email.recipient) != itarget.recipient_len) {
+		err = got_error(GOT_ERR_PRIVSEP_LEN);
+		goto done;
+	}
+	
+	if (itarget.responder_len) {
+		target->conf.email.responder = strndup(imsg->data +
+		    sizeof(itarget) + itarget.sender_len +
+		    itarget.recipient_len, itarget.responder_len);
+		if (target->conf.email.responder == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if (strlen(target->conf.email.responder) !=
+		    itarget.responder_len) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			goto done;
+		}
+	}
+
+	if (itarget.hostname_len) {
+		target->conf.email.hostname = strndup(imsg->data +
+		    sizeof(itarget) + itarget.sender_len +
+		    itarget.recipient_len + itarget.responder_len,
+		    itarget.hostname_len);
+		if (target->conf.email.hostname == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if (strlen(target->conf.email.hostname) !=
+		    itarget.hostname_len) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			goto done;
+		}
+	}
+
+	if (itarget.port_len) {
+		target->conf.email.port = strndup(imsg->data +
+		    sizeof(itarget) + itarget.sender_len +
+		    itarget.recipient_len + itarget.responder_len +
+		    itarget.hostname_len, itarget.port_len);
+		if (target->conf.email.port == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if (strlen(target->conf.email.port) != itarget.port_len) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			goto done;
+		}
+	}
+
+	if (repo_name) {
+		*repo_name = strndup(imsg->data +
+		    sizeof(itarget) + itarget.sender_len +
+		    itarget.recipient_len + itarget.responder_len +
+		    itarget.hostname_len + itarget.port_len,
+		    itarget.repo_name_len);
+		if (*repo_name == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if (strlen(*repo_name) != itarget.repo_name_len) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			free(*repo_name);
+			*repo_name = NULL;
+			goto done;
+		}
+	}
+
+	*new_target = target;
+done:
+	if (err)
+		gotsys_notification_target_free(target);
+	return err;
+}
+
+const struct got_error *
+gotsys_imsg_recv_notification_target_http(char **repo_name,
+    struct gotsys_notification_target **new_target, struct imsg *imsg)
+{
+	const struct got_error *err = NULL;
+	struct gotsysd_imsg_notitfication_target_http itarget;
+	struct gotsys_notification_target *target;
+	size_t datalen;
+
+	if (repo_name)
+		*repo_name = NULL;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen < sizeof(itarget))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&itarget, imsg->data, sizeof(itarget));
+
+	if (datalen != sizeof(itarget) + itarget.hostname_len +
+	    itarget.port_len + itarget.path_len + itarget.user_len +
+	    itarget.password_len + itarget.hmac_len + itarget.repo_name_len)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	if (itarget.hostname_len == 0 || itarget.port_len == 0 ||
+	    itarget.path_len == 0 || itarget.repo_name_len == 0)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	target = calloc(1, sizeof(*target));
+	if (target == NULL)
+		return got_error_from_errno("calloc");
+
+	target->type = GOTSYS_NOTIFICATION_VIA_HTTP;
+
+	target->conf.http.tls = itarget.tls;
+
+	target->conf.http.hostname = strndup(imsg->data +
+	    sizeof(itarget), itarget.hostname_len);
+	if (target->conf.http.hostname == NULL) {
+		err = got_error_from_errno("strndup");
+		goto done;
+	}
+	if (strlen(target->conf.http.hostname) != itarget.hostname_len) {
+		err = got_error(GOT_ERR_PRIVSEP_LEN);
+		goto done;
+	}
+
+	target->conf.http.port = strndup(imsg->data + sizeof(itarget) +
+	    itarget.hostname_len, itarget.port_len);
+	if (target->conf.http.port == NULL) {
+		err = got_error_from_errno("strndup");
+		goto done;
+	}
+	if (strlen(target->conf.http.port) != itarget.port_len) {
+		err = got_error(GOT_ERR_PRIVSEP_LEN);
+		goto done;
+	}
+	
+	target->conf.http.path = strndup(imsg->data +
+	    sizeof(itarget) + itarget.hostname_len + itarget.port_len,
+	    itarget.path_len);
+	if (target->conf.http.path == NULL) {
+		err = got_error_from_errno("strndup");
+		goto done;
+	}
+	if (strlen(target->conf.http.path) != itarget.path_len) {
+		err = got_error(GOT_ERR_PRIVSEP_LEN);
+		goto done;
+	}
+
+	if (itarget.user_len) {
+		target->conf.http.user = strndup(imsg->data +
+		    sizeof(itarget) + itarget.hostname_len +
+		    itarget.port_len + itarget.path_len,
+		    itarget.user_len);
+		if (target->conf.http.user == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if (strlen(target->conf.http.user) != itarget.user_len) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			goto done;
+		}
+	}
+
+	if (itarget.password_len) {
+		target->conf.http.password = strndup(imsg->data +
+		    sizeof(itarget) + itarget.hostname_len +
+		    itarget.port_len + itarget.path_len + itarget.user_len,
+		    itarget.password_len);
+		if (target->conf.http.password == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if (strlen(target->conf.http.password) !=
+		    itarget.password_len) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			goto done;
+		}
+	}
+
+	if (itarget.hmac_len) {
+		target->conf.http.hmac_secret = strndup(imsg->data +
+		    sizeof(itarget) + itarget.hostname_len +
+		    itarget.port_len + itarget.path_len +
+		    itarget.user_len + itarget.password_len,
+		    itarget.hmac_len);
+		if (target->conf.http.hmac_secret == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if (strlen(target->conf.http.hmac_secret) != itarget.hmac_len) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			goto done;
+		}
+	}
+
+	if (repo_name) {
+		*repo_name = strndup(imsg->data +
+		    sizeof(itarget) + itarget.hostname_len +
+		    itarget.port_len + itarget.path_len +
+		    itarget.user_len + itarget.password_len +
+		    itarget.hmac_len, itarget.repo_name_len);
+		if (*repo_name == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if (strlen(*repo_name) != itarget.repo_name_len) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			free(*repo_name);
+			*repo_name = NULL;
+			goto done;
+		}
+	}
+		
+	*new_target = target;
+done:
+	if (err)
+		gotsys_notification_target_free(target);
+	return err;
+}
blob - 78e4fcdc0abf1630c98b1706dda411909c331c5f
blob + 2f11f478c0f33499ceb77104e13e6862d885be25
--- regress/gotsysd/Makefile
+++ regress/gotsysd/Makefile
@@ -26,12 +26,16 @@ GOTSYSD_VM_PASSWORD?=gameoftrees
 GOTSYSD_VM_PASSWD_FILE=gotsysd_vm_passwd
 GOTD_CONF=gotd.conf
 GOTD_UID=501	# /usr/ports/infrastructure/db/user.list
+GOTD_USER=_gotd
 GOTSYSD_CONF=gotsysd.conf
 GOTSYSD_UID=600	# /usr/ports/infrastructure/db/user.list
 GOTSYS_CONF=gotsys.conf
 GOT_CONF=got.conf
 GOTSYS_REPO=gotsys.git
 GOTWEBD_UID=593	# /usr/ports/infrastructure/db/user.list
+GOTSYSD_TEST_SMTP_PORT=2525
+GOTSYSD_TEST_HTTP_PORT=8000
+GOTSYSD_TEST_HMAC_SECRET!=openssl rand -base64 32
 
 GOTSYSD_TEST_USER?=${DOAS_USER}
 .if empty(GOTSYSD_TEST_USER)
@@ -55,7 +59,11 @@ GOTSYSD_TEST_ENV=GOTSYSD_TEST_ROOT=${GOTSYSD_TEST_ROOT
 	GOTSYSD_SSH_PUBKEY=${GOTSYSD_SSH_PUBKEY} \
 	GOTSYS_REPO=${GOTSYS_REPO} \
 	HOME=$(GOTSYSD_TEST_USER_HOME) \
-	PATH=$(GOTSYSD_TEST_USER_HOME)/bin:$(PATH)
+	PATH=$(GOTSYSD_TEST_USER_HOME)/bin:$(PATH) \
+	GOTD_USER=${GOTD_USER} \
+	GOTSYSD_TEST_SMTP_PORT=${GOTSYSD_TEST_SMTP_PORT} \
+	GOTSYSD_TEST_HTTP_PORT=${GOTSYSD_TEST_HTTP_PORT} \
+	GOTSYSD_TEST_HMAC_SECRET=${GOTSYSD_TEST_HMAC_SECRET}
 
 
 UNPRIV=su -m ${GOTSYSD_TEST_USER} -c
@@ -270,6 +278,8 @@ test_gotsysd: 
 	VMID=`vmctl status ${GOTSYSD_VM_NAME} | tail -n1 | \
 		awk '{print $$1}'`; \
 	VMIP="100.64.$$VMID.3"; \
-	${UNPRIV} "env ${GOTSYSD_TEST_ENV} VMIP=$${VMIP} sh ./test_gotsysd.sh"
+	GWIP="100.64.$$VMID.2"; \
+	${UNPRIV} "env ${GOTSYSD_TEST_ENV} VMIP=$${VMIP} GWIP=$${GWIP} \
+		sh ./test_gotsysd.sh"
 
 .include <bsd.regress.mk>
blob - d8a6a0669ccae57aaf4c5fa85f67acd017b0f6be
blob + dece9a715ab4b1fb2ed8745339a3cf7f77e76655
--- regress/gotsysd/test_gotsysd.sh
+++ regress/gotsysd/test_gotsysd.sh
@@ -1878,10 +1878,486 @@ test_override_all_user_access() {
 			return 1
 		fi
 	done
+
+	# Undo gotsys.conf override
+	ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} 'rm -f /etc/gotsysd.conf'
+
+	# Restart gotsysd (XXX need a better way to do this...)
+	ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} 'pkill -xf /usr/local/sbin/gotsysd'
+	sleep 1
+	ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} '/usr/local/sbin/gotsysd -vvv'
+	sleep 1
+	ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} 'gotsys apply -w' > /dev/null
 
 	test_done "$testroot" "$ret"
 }
 
+# flan:password encoded in base64
+AUTH="ZmxhbjpwYXNzd29yZA=="
+
+test_http_notification() {
+	local testroot=`test_init http_notification 1`
+
+	got checkout -q $testroot/${GOTSYS_REPO} $testroot/wt >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'`
+	crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'`
+	sshkey=`cat ${GOTSYSD_SSH_PUBKEY}`
+	cat > ${testroot}/wt/gotsys.conf <<EOF
+group slackers
+
+user ${GOTSYSD_TEST_USER} {
+	password "${crypted_vm_pw}" 
+	authorized key ${sshkey}
+}
+user ${GOTSYSD_DEV_USER} {
+	password "${crypted_pw}" 
+	authorized key ${sshkey}
+}
+repository gotsys.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+
+	notify url "http://${GWIP}:${GOTSYSD_TEST_HTTP_PORT}/" user flan password "password" insecure
+}
+EOF
+	(cd ${testroot}/wt && got commit -m "send http notifications" \
+		>/dev/null)
+	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
+
+	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO}
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	# Wait for gotsysd to apply the new configuration.
+	echo "$commit_id" > $testroot/stdout.expected
+	for i in 1 2 3 4 5; do
+		sleep 1
+		ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+			cat /var/db/gotsysd/commit > $testroot/stdout
+		if cmp -s $testroot/stdout.expected $testroot/stdout; then
+			break;
+		fi
+	done
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "gotsysd failed to apply configuration" >&2
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cat > ${testroot}/wt/gotsys.conf <<EOF
+group slackers
+
+user ${GOTSYSD_TEST_USER} {
+	password "${crypted_vm_pw}" 
+	authorized key ${sshkey}
+}
+
+user ${GOTSYSD_DEV_USER} {
+	password "${crypted_pw}" 
+	authorized key ${sshkey}
+}
+
+repository gotsys.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+
+	notify url "http://${GWIP}:${GOTSYSD_TEST_HTTP_PORT}/" user flan password "password" insecure
+}
+EOF
+
+	(cd ${testroot}/wt && got commit -m "whitespace changes" >/dev/null)
+	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
+	local author_time=`git_show_author_time $testroot/${GOTSYS_REPO}`
+
+	timeout 5 ./http-server -a $AUTH -l "$GWIP" \
+	    -p "$GOTSYSD_TEST_HTTP_PORT" > $testroot/stdout &
+
+	sleep 1 # server starts up
+
+	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO}
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	wait %1 # wait for the http "server"
+
+	echo -n > "$testroot/stdout.expected"
+	ed -s "$testroot/stdout.expected" <<-EOF
+	a
+	{"notifications":[{
+		"type":"commit",
+		"short":false,
+		"repo":"gotsys.git",
+		"authenticated_user":"${GOTSYSD_TEST_USER}",
+		"id":"$commit_id",
+		"author":{
+			"full":"$GOT_AUTHOR",
+			"name":"$GIT_AUTHOR_NAME",
+			"mail":"$GIT_AUTHOR_EMAIL",
+			"user":"$GOT_AUTHOR_11"
+		},
+		"committer":{
+			"full":"$GOT_AUTHOR",
+			"name":"$GIT_AUTHOR_NAME",
+			"mail":"$GIT_AUTHOR_EMAIL",
+			"user":"$GOT_AUTHOR_11"
+		},
+		"date":$author_time,
+		"short_message":"whitespace changes",
+		"message":"whitespace changes\n",
+		"diffstat":{
+			"files":[{
+				"action":"modified",
+				"file":"gotsys.conf",
+				"added":2,
+				"removed":0
+			}],
+			"total":{
+				"added":1,
+				"removed":2
+			}
+		}
+	}]}
+	.
+	,j
+	w
+	EOF
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_http_notification_hmac() {
+	local testroot=`test_init http_notification_hmac 1`
+
+	got checkout -q $testroot/${GOTSYS_REPO} $testroot/wt >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'`
+	crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'`
+	sshkey=`cat ${GOTSYSD_SSH_PUBKEY}`
+	cat > ${testroot}/wt/gotsys.conf <<EOF
+group slackers
+
+user ${GOTSYSD_TEST_USER} {
+	password "${crypted_vm_pw}" 
+	authorized key ${sshkey}
+}
+
+user ${GOTSYSD_DEV_USER} {
+	password "${crypted_pw}" 
+	authorized key ${sshkey}
+}
+
+repository gotsys.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+
+	notify url "http://${GWIP}:${GOTSYSD_TEST_HTTP_PORT}/" user flan password "password" insecure hmac "${GOTSYSD_TEST_HMAC_SECRET}"
+}
+EOF
+
+	(cd ${testroot}/wt && got commit -m "add hmac" >/dev/null)
+	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
+
+	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO}
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	# Wait for gotsysd to apply the new configuration.
+	echo "$commit_id" > $testroot/stdout.expected
+	for i in 1 2 3 4 5; do
+		sleep 1
+		ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+			cat /var/db/gotsysd/commit > $testroot/stdout
+		if cmp -s $testroot/stdout.expected $testroot/stdout; then
+			break;
+		fi
+	done
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "gotsysd failed to apply configuration" >&2
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cat > ${testroot}/wt/gotsys.conf <<EOF
+group slackers
+
+user ${GOTSYSD_TEST_USER} {
+	password "${crypted_vm_pw}" 
+	authorized key ${sshkey}
+}
+user ${GOTSYSD_DEV_USER} {
+	password "${crypted_pw}" 
+	authorized key ${sshkey}
+}
+repository gotsys.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+	notify url "http://${GWIP}:${GOTSYSD_TEST_HTTP_PORT}/" user flan password "password" insecure hmac "${GOTSYSD_TEST_HMAC_SECRET}"
+}
+EOF
+
+	(cd ${testroot}/wt && got commit -m "whitespace changes" >/dev/null)
+	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
+	local author_time=`git_show_author_time $testroot/${GOTSYS_REPO}`
+
+	timeout 5 ./http-server -a $AUTH -l "$GWIP" \
+	    -p "$GOTSYSD_TEST_HTTP_PORT" -s "$GOTSYSD_TEST_HMAC_SECRET" \
+	    > $testroot/stdout &
+
+	sleep 1 # server starts up
+
+	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO}
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	wait %1 # wait for the http "server"
+
+	echo -n > "$testroot/stdout.expected"
+	ed -s "$testroot/stdout.expected" <<-EOF
+	a
+	{"notifications":[{
+		"type":"commit",
+		"short":false,
+		"repo":"gotsys.git",
+		"authenticated_user":"${GOTSYSD_TEST_USER}",
+		"id":"$commit_id",
+		"author":{
+			"full":"$GOT_AUTHOR",
+			"name":"$GIT_AUTHOR_NAME",
+			"mail":"$GIT_AUTHOR_EMAIL",
+			"user":"$GOT_AUTHOR_11"
+		},
+		"committer":{
+			"full":"$GOT_AUTHOR",
+			"name":"$GIT_AUTHOR_NAME",
+			"mail":"$GIT_AUTHOR_EMAIL",
+			"user":"$GOT_AUTHOR_11"
+		},
+		"date":$author_time,
+		"short_message":"whitespace changes",
+		"message":"whitespace changes\n",
+		"diffstat":{
+			"files":[{
+				"action":"modified",
+				"file":"gotsys.conf",
+				"added":0,
+				"removed":3
+			}],
+			"total":{
+				"added":1,
+				"removed":0
+			}
+		}
+	}]}
+	.
+	,j
+	w
+	EOF
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_email_notification() {
+	local testroot=`test_init email_notification 1`
+
+	# Need to smtpd in the test VM since we will be using port 25
+	ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+		'/etc/rc.d/smtpd stop' > /dev/null
+
+	got checkout -q $testroot/${GOTSYS_REPO} $testroot/wt >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'`
+	crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'`
+	sshkey=`cat ${GOTSYSD_SSH_PUBKEY}`
+	cat > ${testroot}/wt/gotsys.conf <<EOF
+group slackers
+
+user ${GOTSYSD_TEST_USER} {
+	password "${crypted_vm_pw}" 
+	authorized key ${sshkey}
+}
+user ${GOTSYSD_DEV_USER} {
+	password "${crypted_pw}" 
+	authorized key ${sshkey}
+}
+repository gotsys.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+	notify email to "${GOTSYSD_TEST_USER}@example.com"
+}
+EOF
+	(cd ${testroot}/wt && got commit -m "send email notifications" \
+		>/dev/null)
+	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
+
+	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO}
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	# Wait for gotsysd to apply the new configuration.
+	echo "$commit_id" > $testroot/stdout.expected
+	for i in 1 2 3 4 5; do
+		sleep 1
+		ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+			cat /var/db/gotsysd/commit > $testroot/stdout
+		if cmp -s $testroot/stdout.expected $testroot/stdout; then
+			break;
+		fi
+	done
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "gotsysd failed to apply configuration" >&2
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cat > ${testroot}/wt/gotsys.conf <<EOF
+group slackers
+
+user ${GOTSYSD_TEST_USER} {
+	password "${crypted_vm_pw}" 
+	authorized key ${sshkey}
+}
+
+user ${GOTSYSD_DEV_USER} {
+	password "${crypted_pw}" 
+	authorized key ${sshkey}
+}
+
+repository gotsys.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+
+	notify email to "${GOTSYSD_TEST_USER}@example.com"
+}
+EOF
+	(cd ${testroot}/wt && got commit -m "whitespace changes" >/dev/null)
+	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
+	local author_time=`git_show_author_time $testroot/${GOTSYS_REPO}`
+
+	ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+		'printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \
+		| timeout 5 nc -l 25' > $testroot/stdout &
+
+	sleep 1 # server starts up
+
+	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO}
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	wait %1 # wait for ssh / nc -l
+
+	short_commit_id=`trim_obj_id 12 $commit_id`
+	HOSTNAME=`ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} hostname`
+	printf "HELO localhost\r\n" > $testroot/stdout.expected
+	printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \
+		>> $testroot/stdout.expected
+	printf "RCPT TO:<${GOTSYSD_TEST_USER}@example.com>\r\n" \
+		>> $testroot/stdout.expected
+	printf "DATA\r\n" >> $testroot/stdout.expected
+	printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" >> $testroot/stdout.expected
+	printf "To: ${GOTSYSD_TEST_USER}@example.com\r\n" \
+		>> $testroot/stdout.expected
+	printf "Subject: $GOTSYS_REPO: " >> $testroot/stdout.expected
+	printf "${GOTSYSD_TEST_USER} changed refs/heads/main: $short_commit_id\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 "messagelen: 20\n" >> $testroot/stdout.expected
+	printf " \n" >> $testroot/stdout.expected
+	printf " whitespace changes\n \n" >> $testroot/stdout.expected
+	printf " M  gotsys.conf  |  3+  0-\n\n"  >> $testroot/stdout.expected
+	printf "1 file changed, 3 insertions(+), 0 deletions(-)\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
+
+	# Restart smtpd 
+	ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+		'/etc/rc.d/smtpd start' > /dev/null
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_user_add
 run_test test_user_mod
@@ -1898,3 +2374,6 @@ run_test test_protect_refs
 run_test test_deny_access
 run_test test_override_access_rules
 run_test test_override_all_user_access
+run_test test_http_notification
+run_test test_http_notification_hmac
+run_test test_email_notification
blob - /dev/null
blob + b3f5e0351cc4e811b08b7d4b7850133a15cfc4ae (mode 755)
--- /dev/null
+++ regress/gotsysd/http-server
@@ -0,0 +1,129 @@
+#!/usr/bin/env perl
+#
+# Copyright (c) 2024 Omar Polo <op@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use v5.36;
+use IPC::Open2;
+use Getopt::Long qw(:config bundling no_getopt_compat);
+use Digest;
+use Digest::HMAC;
+
+my $auth;
+my $address;
+my $port = 8000;
+my $hmac_secret;
+my $hmac_signature;
+my $hmac;
+
+GetOptions("a:s" => \$auth, "l:s" => \$address, "p:i" => \$port, "s:s" => \$hmac_secret)
+    or die("usage: $0 [-a auth] [-l address] [-p port] [-s hmac_secret]\n");
+
+my $pid = open2(my $out, my $in, 'nc', '-l', $address, $port);
+
+my $clen;
+while (<$out>) {
+	local $/ = "\r\n";
+	chomp;
+
+	last if /^$/;
+
+	if (m/^POST/) {
+		die "bad http request" unless m,^POST / HTTP/1.1$,;
+		next;
+	}
+
+	if (m/^Host:/) {
+		die "bad Host header" unless /^Host: $address:$port$/;
+		next;
+	}
+
+	if (m/^Content-Type/) {
+		die "bad content-type header"
+		    unless m,Content-Type: application/json$,;
+		next;
+	}
+
+	if (m/^Content-Length/) {
+		die "double content-length" if defined $clen;
+		die "bad content-length header"
+		    unless m/Content-Length: (\d+)$/;
+		$clen = $1;
+		next;
+	}
+
+	if (m/Connection/) {
+		die "bad connection header"
+		    unless m/Connection: close$/;
+		next;
+	}
+
+	if (m/Authorization/) {
+		die "bad authorization header"
+		    unless m/Authorization: basic (.*)$/;
+		my $t = $1;
+		die "wrong authorization; got $t want $auth"
+		    if not defined($auth) or $auth ne $t;
+		next;
+	}
+
+	if (m/X-Gotd-Signature/) {
+		die "bad hmac signature header"
+		    unless m/X-Gotd-Signature: sha256=(.*)$/;
+		$hmac_signature = $1;
+		next;
+	}
+}
+
+die "no Content-Length header" unless defined $clen;
+
+if (defined $hmac_signature) {
+	die "no Hmac secret provided" unless defined $hmac_secret;
+	my $sha256 = Digest->new("SHA-256");
+	$hmac = Digest::HMAC->new($hmac_secret, $sha256);
+}
+
+while ($clen != 0) {
+	my $len = $clen;
+	$len = 512 if $clen > 512;
+
+	my $r = read($out, my $buf, $len);
+	$clen -= $r;
+
+	if (defined $hmac) {
+		$hmac->add($buf);
+	}
+
+	print $buf;
+}
+say "";
+
+if (defined $hmac) {
+	my $digest = $hmac->hexdigest;
+	if ($digest ne $hmac_signature) {
+		print "bad hmac signature: expected: $hmac_signature, actual: $digest";
+		die
+	}
+}
+
+print $in "HTTP/1.1 200 OK\r\n";
+print $in "Content-Length: 0\r\n";
+print $in "Connection: close\r\n";
+print $in "\r\n";
+
+close $in;
+close $out;
+
+waitpid($pid, 0);
+exit $? >> 8;