From: "Omar Polo" Subject: Re: gotsysd notification support To: Stefan Sperling Cc: gameoftrees@openbsd.org Date: Thu, 24 Jul 2025 01:10:51 +0200 Stefan Sperling wrote: > This patch adds support for email and http notifications to gotsysd > and gotsys.conf. > > HTTP notifications are sent directly by got-notify-http and will go > anywhere the gotsys.conf author wants, provided the server allows > outgoing TCP connections to the destination. > > Email notifications are only sent via localhost:25 because sending email > might require a smarthost or other custom MTA configuration. I would > rather leave this under full control of the server administrator instead > of trying to second-guess them and automate something which maybe won't work. > > There are some restrictions regarding characters allowed in notification > parameters, such as email addreses and URLs. My concern is that data from > gotsys.conf ends up in gotd.conf and I don't want people to play tricks > by escaping from a string with the " character and then writing arbitrary > content into the generated gotd.conf file. URL-encoding of HTTP notification > request paths is done for the same reason. > > Documentation updates and test coverage are included. > > ok? wow this is long :-) i probably need another read. left some nitpicks but globally i think we want this in as soon as possible and eventually fix/address stuff in tree. the only thing that makes me a bit uncomfortable is some of the validation we do. don't get me wrong, i love the degree of validation that you're doing, but in a few cases (email) i think it might be a bit too much, and just a general check that it cannot break the generated config could be enough. but i also see the value in being more strict upfront. > M gotsys/gotsys.conf.5 | 241+ 127- > M gotsys/gotsys.h | 2+ 0- > M gotsys/parse.y | 287+ 15- > M gotsysd/gotsysd.h | 50+ 0- > M gotsysd/helpers.c | 18+ 0- > M gotsysd/libexec/gotsys-write-conf/gotsys-write-conf.c | 519+ 1- > M gotsysd/sysconf.c | 145+ 1- > M lib/gotsys_imsg.c | 522+ 1- > M regress/gotsysd/Makefile | 12+ 2- > A regress/gotsysd/http-server | 129+ 0- > M regress/gotsysd/test_gotsysd.sh | 479+ 0- > > 11 files changed, 2404 insertions(+), 147 deletions(-) > > commit - 41fda6f0f9b9dd737c3e67642f92562b4fac35f7 > commit + 386afa46ae3327ed4d895148a9879c42974c4520 > blob - de9309635fb9ef5f8fdebd0e38d618e1d2798de4 > blob + ec5b763882da5225867b0dc9bbcd2f7cf279a328 > --- gotsys/gotsys.conf.5 > +++ gotsys/gotsys.conf.5 > @@ -391,128 +391,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 > @@ -546,12 +660,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 - 3b4293f212cd39c09fc0b944debb82296d969d75 > blob + c3480115504c26fbb8fed32ea51f34ad55dab693 > --- gotsys/gotsys.h > +++ gotsys/gotsys.h > @@ -106,7 +106,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 - 9b8f7591885f6d4d616238ab087b01a936bb871e > blob + a68d7c4b162a3cd5f9cb7f9390c8ecb2bc688b48 > --- gotsys/parse.y > +++ gotsys/parse.y > @@ -1315,6 +1315,8 @@ conf_notify_branch(struct gotsys_repo *repo, char *bra > } > if (pe == NULL) > free(refname); > + else > + repo->num_notification_refs++; > > return 0; > } > @@ -1344,11 +1346,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) > +{ to be fair I'm not sure I'd like to valide the mail address like this. there are some gotchas in the format. can't we just traverse the string and make sure all the characters are printable and don't include \ or quotes instead? feels way more simple. > + 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])) nit but I thought we have to cast this as if (isalnum((unsigned char)s[i])) to provent issues when char is signed. > + 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) > { > @@ -1371,18 +1432,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)); > @@ -1412,18 +1486,97 @@ free_target: > return -1; > } > > +static inline int > +should_urlencode(int c) maybe we should move this to some utils :-) > +{ > + 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) { > @@ -1433,10 +1586,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]; i know it's impossible at this point for url to be "", but can't we write it in a sligthly more readable and less worrying way? like p = strchr(url, '\0'); > > q = memchr(s, ':', p - s); > if (q) { > @@ -1458,6 +1609,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) { > @@ -1465,22 +1627,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]) || same, shouldn't this be isalnum((unsigned char)(*host)[i]) ? (i won't highlight future is*() in case i'm wrong. > + (*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); > @@ -1496,6 +1671,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++) { nit, but it triggers me a bit to traverse a string twice :p for (i = 0; s[i] != '\0'; ++i) ? > + if (s[i] & 0x80) > + return 0; is this to "protect" isalnum? > + 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[] = should this be uint8_t instead? > + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; > + size_t i; > + > + for (i = 0; i < len; i++) { > + if (strchr(base64chars, s[i]) == NULL) > + return 0; > + } not really an issue but I would have used strspn() here instead of looping over the given string. (also because this is only called with a proper C string. > + return 1; > +} > + > +static int > conf_notify_http(struct gotsys_repo *repo, char *url, char *user, > char *password, int insecure, char *hmac_secret) > { > @@ -1573,11 +1810,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)); > @@ -1586,6 +1848,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 - b6a2d93351ce9620926282a1ad82b34e21529cc6 > blob + 796adae0e635f2e520dc268d3131865039506913 > --- 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. */ > @@ -447,6 +460,38 @@ struct gotsysd_imsg_pathlist_elem { > /* 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 > @@ -540,6 +585,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 *, > @@ -570,6 +616,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, > @@ -323,6 +329,349 @@ done: > } > > static const struct got_error * > +write_notification_target_email(struct gotsys_notification_target *target) > +{ > + char sender[128]; > + char recipient[128]; > + char responder[128]; where these 128 comes from? > + 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); sorry but i'm just wondering if we can avoid the headache of having enough room in sender, recipient and responder and just call multiple times dprintf() in here. > + 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); same here. > + 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); I'm actually wondering whether this can really happen. The manpage says RETURN VALUES For all these functions if an output or encoding error occurs, a value less than 0 is returned. and if i'm reading libc correctly the actual write is done by __sfvwrite() which returns EOF on error, even if we managed to write something. so this should not happen. > + } > + > + 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; > + 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); > + } > + > + 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); > + if (ret != 2 + strlen(closing)) > + return got_error_fmt(GOT_ERR_IO, "short write to %s", > + 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 - 7fee8a40df4446794f8d3b8d9b88ebcfe89559f4 > blob + 554badbe0a0be65f91f07315a9eaa4cf0ddb2391 > --- 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 - be6c5e0e7bf717619f74605bb70de1188f08480e > blob + f1655dcef5d0d634b534c6bce9610af42b951d0f > --- lib/gotsys_imsg.c > +++ lib/gotsys_imsg.c > @@ -747,6 +747,259 @@ send_protected_refs(struct gotsysd_imsgev *iev, struct > } > > 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; > @@ -791,7 +1044,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; > } > @@ -984,3 +1239,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 > blob - a26ccdab2420b379ec584c361b270ed32e8b1b2a > blob + b6994f1bdd6552342b6c2ebc323c4cb5cc1762b7 > --- regress/gotsysd/test_gotsysd.sh > +++ regress/gotsysd/test_gotsysd.sh > @@ -1879,9 +1879,485 @@ test_override_all_user_access() { > 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 < +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 < +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 < +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 < +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 < +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 < +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 > +# > +# 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;