From: Stefan Sperling Subject: gotsysd notification support To: gameoftrees@openbsd.org Date: Tue, 22 Jul 2025 16:52:32 +0200 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? 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) +{ + 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) { @@ -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) +{ + 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]; 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]) || + (*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++) { + 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) { @@ -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]; + 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; + 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 </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 </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 </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 </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 </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 </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;