commit 8b4b261f2806a6ddbcf0994f02f3ba4502828e8e from: Stefan Sperling date: Thu Jul 24 07:25:49 2025 UTC add support for notifications to gotsysd and gotsys.conf gotsys.conf now supports email and HTTP/JSON notifications ok op@ commit - 41fda6f0f9b9dd737c3e67642f92562b4fac35f7 commit + 8b4b261f2806a6ddbcf0994f02f3ba4502828e8e 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. */ @@ -445,8 +458,40 @@ struct gotsysd_imsg_pathlist_elem { /* Followed by path_len bytes. */ /* Followed by data_len bytes. */ +}; + +/* Structure for GOTSYSD_IMSG_NOTIFICATION_TARGET_EMAIL. */ +struct gotsysd_imsg_notitfication_target_email { + size_t sender_len; + size_t recipient_len; + size_t responder_len; + size_t hostname_len; + size_t port_len; + size_t repo_name_len; + + /* + * Followed by sender_len + responder_len + responder_len + + * hostname_len + port_len + repo_name_len bytes. + */ }; +/* Structure for GOTD_IMSG_NOTIFICATION_TARGET_HTTP. */ +struct gotsysd_imsg_notitfication_target_http { + int tls; + size_t hostname_len; + size_t port_len; + size_t path_len; + size_t user_len; + size_t password_len; + size_t hmac_len; + size_t repo_name_len; + + /* + * Followed by hostname_len + port_len + path_len + user_len + password_len + + * hmac_len + repo_name_len bytes. + */ +}; + #ifndef GOT_LIBEXECDIR #define GOT_LIBEXECDIR /usr/libexec #endif @@ -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, @@ -298,6 +304,206 @@ write_protected_refs(struct gotsys_repo *repo) } RB_FOREACH(pe, got_pathlist_head, &repo->protected_branches) { + err = refname_is_valid(pe->path); + if (err) + return err; + ret = dprintf(gotd_conf_tmpfd, "\t\tbranch \"%s\"\n", pe->path); + if (ret == -1) { + return got_error_from_errno2("dprintf", + gotd_conf_tmppath); + } + if (ret != 12 + strlen(pe->path)) + return got_error_fmt(GOT_ERR_IO, "short write to %s", + gotd_conf_tmppath); + } + + ret = dprintf(gotd_conf_tmpfd, "\t%s\n", closing); + if (ret == -1) + return got_error_from_errno2("dprintf", gotd_conf_tmppath); + if (ret != 2 + strlen(closing)) + return got_error_fmt(GOT_ERR_IO, "short write to %s", + gotd_conf_tmppath); +done: + free(namespace); + return NULL; +} + +static const struct got_error * +write_notification_target_email(struct gotsys_notification_target *target) +{ + char sender[128]; + char recipient[128]; + char responder[128]; + int ret = 0; + + if (target->conf.email.sender) { + ret = snprintf(sender, sizeof(sender), " from \"%s\"", + target->conf.email.sender); + if (ret == -1) + return got_error_from_errno("snprintf"); + if ((size_t)ret >= sizeof(sender)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "notification email sender too long"); + } + } else + sender[0] = '\0'; + + ret = snprintf(recipient, sizeof(recipient), " to \"%s\"", + target->conf.email.recipient); + if (ret == -1) + return got_error_from_errno("snprintf"); + if ((size_t)ret >= sizeof(recipient)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "notification email recipient too long"); + } + + if (target->conf.email.responder) { + ret = snprintf(responder, sizeof(responder), " reply to \"%s\"", + target->conf.email.responder); + if (ret == -1) + return got_error_from_errno("snprintf"); + if ((size_t)ret >= sizeof(responder)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "notification email responder too long"); + } + } else + responder[0] = '\0'; + + ret = dprintf(gotd_conf_tmpfd, "\t\temail%s%s%s\n", + sender, recipient, responder); + if (ret == -1) + return got_error_from_errno2("dprintf", gotd_conf_tmppath); + if (ret != 8 + strlen(sender) + strlen(recipient) + strlen(responder)) { + return got_error_fmt(GOT_ERR_IO, "short write to %s", + gotd_conf_tmppath); + } + + return NULL; +} + +static const struct got_error * +write_notification_target_http(struct gotsys_notification_target *target, + int idx) +{ + char proto[16]; + char port[16]; + char label[16]; + char auth[128]; + char insecure[16]; + char hmac[128]; + int ret = 0; + + insecure[0] = '\0'; + + if (target->conf.http.tls) { + if (strlcpy(proto, "https://", sizeof(proto)) >= + sizeof(proto)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "http notification protocol too long"); + } + } else { + if (strlcpy(proto, "http://", sizeof(proto)) >= + sizeof(proto)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "http notification protocol too long"); + } + + if (target->conf.http.user && target->conf.http.password) { + if (strlcpy(insecure, " insecure", sizeof(insecure)) >= + sizeof(insecure)) { + return got_error_msg(GOT_ERR_NO_SPACE, "http " + "notification insecure keyword too long"); + } + } + } + + if (target->conf.http.port) { + ret = snprintf(port, sizeof(port), ":%s", + target->conf.http.port); + if (ret == -1) + return got_error_from_errno("snprintf"); + if ((size_t)ret >= sizeof(port)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "notification http port too long"); + } + } else + port[0] = '\0'; + + if (target->conf.http.user && target->conf.http.password) { + ret = snprintf(label, sizeof(label), "basic%d", idx); + if (ret == -1) + return got_error_from_errno("snprintf"); + if ((size_t)ret >= sizeof(label)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "basic auth label too long"); + } + + ret = snprintf(auth, sizeof(auth), " auth %s", label); + if (ret == -1) + return got_error_from_errno("snprintf"); + if ((size_t)ret >= sizeof(label)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "http notification auth too long"); + } + } else + auth[0] = '\0'; + + if (target->conf.http.hmac_secret) { + ret = snprintf(label, sizeof(label), "hmac%d", idx); + if (ret == -1) + return got_error_from_errno("snprintf"); + if ((size_t)ret >= sizeof(label)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "notification http hmac label too long"); + } + + ret = snprintf(hmac, sizeof(hmac), " hmac %s", label); + if (ret == -1) + return got_error_from_errno("snprintf"); + if ((size_t)ret >= sizeof(label)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "http notification hmac too long"); + } + } else + hmac[0] = '\0'; + + ret = dprintf(gotd_conf_tmpfd, "\t\turl \"%s%s%s/%s\"%s%s%s\n", + proto, target->conf.http.hostname, port, + target->conf.http.path, auth, insecure, hmac); + if (ret == -1) + return got_error_from_errno2("dprintf", gotd_conf_tmppath); + if (ret != 10 + strlen(proto) + strlen(target->conf.http.hostname) + + strlen(port) + strlen(target->conf.http.path) + strlen(auth) + + strlen(insecure) + strlen(hmac)) { + return got_error_fmt(GOT_ERR_IO, "short write to %s", + gotd_conf_tmppath); + } + + return NULL; +} + +static const struct got_error * +write_notification_targets(struct gotsys_repo *repo) +{ + const struct got_error *err = NULL; + struct got_pathlist_entry *pe; + struct gotsys_notification_target *target; + const char *opening = "notify {"; + const char *closing = "}"; + char *namespace = NULL; + int ret = 0, i; + + if (STAILQ_EMPTY(&repo->notification_targets)) + return NULL; + + ret = dprintf(gotd_conf_tmpfd, "\t%s\n", opening); + if (ret == -1) + return got_error_from_errno2("dprintf", gotd_conf_tmppath); + if (ret != 2 + strlen(opening)) + return got_error_fmt(GOT_ERR_IO, "short write to %s", + gotd_conf_tmppath); + + RB_FOREACH(pe, got_pathlist_head, &repo->notification_refs) { err = refname_is_valid(pe->path); if (err) return err; @@ -311,6 +517,47 @@ write_protected_refs(struct gotsys_repo *repo) gotd_conf_tmppath); } + RB_FOREACH(pe, got_pathlist_head, &repo->notification_ref_namespaces) { + namespace = strdup(pe->path); + if (namespace == NULL) + return got_error_from_errno("strdup"); + + got_path_strip_trailing_slashes(namespace); + err = refname_is_valid(namespace); + if (err) + goto done; + + ret = dprintf(gotd_conf_tmpfd, + "\t\treference namespace \"%s\"\n", namespace); + if (ret == -1) { + err = got_error_from_errno2("dprintf", + gotd_conf_tmppath); + goto done; + } + if (ret != 25 + strlen(namespace)) { + err = got_error_fmt(GOT_ERR_IO, "short write to %s", + gotd_conf_tmppath); + goto done; + } + free(namespace); + namespace = NULL; + } + + i = 0; + STAILQ_FOREACH(target, &repo->notification_targets, entry) { + i++; + switch (target->type) { + case GOTSYS_NOTIFICATION_VIA_EMAIL: + err = write_notification_target_email(target); + break; + case GOTSYS_NOTIFICATION_VIA_HTTP: + err = write_notification_target_http(target, i); + break; + default: + break; + } + } + ret = dprintf(gotd_conf_tmpfd, "\t%s\n", closing); if (ret == -1) return got_error_from_errno2("dprintf", gotd_conf_tmppath); @@ -319,10 +566,112 @@ write_protected_refs(struct gotsys_repo *repo) gotd_conf_tmppath); done: free(namespace); + return err; +} + +static const struct got_error * +write_repo_secrets(off_t *written, struct gotsys_repo *repo) +{ + struct gotsys_notification_target *target; + char label[32]; + int ret = 0, i = 0; + size_t len; + + STAILQ_FOREACH(target, &repo->notification_targets, entry) { + if (target->type != GOTSYS_NOTIFICATION_VIA_HTTP) + continue; + + if (target->conf.http.user == NULL && + target->conf.http.password == NULL && + target->conf.http.hmac_secret == NULL) + continue; + + i++; + + if (target->conf.http.user && target->conf.http.password) { + ret = snprintf(label, sizeof(label), "basic%d", i); + if (ret == -1) + return got_error_from_errno("snprintf"); + if ((size_t)ret >= sizeof(label)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "basic auth label too long"); + } + + ret = dprintf(gotd_secrets_tmpfd, + "auth %s user \"%s\" password \"%s\"\n", label, + target->conf.http.user, target->conf.http.password); + if (ret == -1) + return got_error_from_errno2("dprintf", + gotd_secrets_tmppath); + len = strlen(label) + + strlen(target->conf.http.user) + + strlen(target->conf.http.password); + if (ret != 26 + len) { + return got_error_fmt(GOT_ERR_IO, + "short write to %s", gotd_secrets_tmppath); + } + *written += ret; + } + + if (target->conf.http.hmac_secret) { + ret = snprintf(label, sizeof(label), "hmac%d", i); + if (ret == -1) + return got_error_from_errno("snprintf"); + if ((size_t)ret >= sizeof(label)) { + return got_error_msg(GOT_ERR_NO_SPACE, + "hmac secret label too long"); + } + ret = dprintf(gotd_secrets_tmpfd, "hmac %s \"%s\"\n", + label, target->conf.http.hmac_secret); + if (ret == -1) + return got_error_from_errno2("dprintf", + gotd_secrets_tmppath); + len = strlen(label) + + strlen(target->conf.http.hmac_secret); + if (ret != 9 + len) { + return got_error_fmt(GOT_ERR_IO, + "short write to %s", gotd_secrets_tmppath); + } + *written += ret; + } + } + return NULL; } static const struct got_error * +prepare_gotd_secrets(void) +{ + const struct got_error *err = NULL; + struct gotsys_repo *repo; + off_t written = 0; + + if (ftruncate(gotd_secrets_tmpfd, 0) == -1) + return got_error_from_errno("ftruncate"); + + TAILQ_FOREACH(repo, &gotsysconf.repos, entry) { + err = write_repo_secrets(&written, repo); + if (err) + return err; + } + + if (written == 0) { + if (unlink(gotd_secrets_tmppath) == -1) { + return got_error_from_errno2("unlink", + gotd_secrets_tmppath); + } + free(gotd_secrets_tmppath); + gotd_secrets_tmppath = NULL; + + if (close(gotd_secrets_tmpfd) == -1) + return got_error_from_errno("close"); + gotd_secrets_tmpfd = -1; + } + + return NULL; +} + +static const struct got_error * write_gotd_conf(void) { const struct got_error *err = NULL; @@ -405,6 +754,10 @@ write_gotd_conf(void) if (err) return err; + err = write_notification_targets(repo); + if (err) + return err; + ret = dprintf(gotd_conf_tmpfd, "}\n"); if (ret == -1) return got_error_from_errno2("dprintf", @@ -415,6 +768,21 @@ write_gotd_conf(void) } } + if (gotd_secrets_tmppath != NULL && gotd_secrets_tmpfd != -1) { + if (fchmod(gotd_secrets_tmpfd, 0600) == -1) { + return got_error_from_errno_fmt("chmod 0600 %s", + gotd_secrets_tmppath); + } + + if (rename(gotd_secrets_tmppath, GOTD_SECRETS_PATH) == -1) { + return got_error_from_errno_fmt("rename %s to %s", + gotd_conf_tmppath, GOTD_SECRETS_PATH); + } + + free(gotd_secrets_tmppath); + gotd_secrets_tmppath = NULL; + } + if (fchmod(gotd_conf_tmpfd, 0644) == -1) { return got_error_from_errno_fmt("chmod 0644 %s", gotd_conf_tmppath); @@ -692,8 +1060,134 @@ dispatch_event(int fd, short event, void *arg) writeconf_state); break; } - repo_cur = NULL; + break; + case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS: + if (repo_cur == NULL || + notif_refs_cur != NULL || + num_notif_refs_needed != 0 || + writeconf_state != WRITECONF_STATE_EXPECT_REPOS) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + break; + } + err = gotsys_imsg_recv_pathlist(&npaths, &imsg); + if (err) + break; + notif_refs_cur = &repo_cur->notification_refs; + num_notif_refs_cur = &repo_cur->num_notification_refs; + num_notif_refs_needed = npaths; + num_notif_refs_received = 0; break; + case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES: + if (repo_cur == NULL || + notif_refs_cur != NULL || + num_notif_refs_needed != 0 || + writeconf_state != WRITECONF_STATE_EXPECT_REPOS) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + break; + } + err = gotsys_imsg_recv_pathlist(&npaths, &imsg); + if (err) + break; + notif_refs_cur = + &repo_cur->notification_ref_namespaces; + num_notif_refs_cur = + &repo_cur->num_notification_ref_namespaces; + num_notif_refs_needed = npaths; + num_notif_refs_received = 0; + break; + case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_ELEM: + case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_ELEM: + if (notif_refs_cur == NULL || + num_notif_refs_cur == NULL || + num_notif_refs_needed == 0 || + num_notif_refs_received >= + num_notif_refs_needed || + writeconf_state != WRITECONF_STATE_EXPECT_REPOS) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + break; + } + err = gotsys_imsg_recv_pathlist_elem(&imsg, + notif_refs_cur); + if (err) + break; + if (++num_notif_refs_received >= + num_notif_refs_needed) { + notif_refs_cur = NULL; + *num_notif_refs_cur = num_notif_refs_received; + num_notif_refs_needed = 0; + num_notif_refs_received = 0; + } + break; + case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_DONE: + if (repo_cur == NULL || + num_notif_refs_needed != 0 || + notif_refs_cur != NULL || + writeconf_state != WRITECONF_STATE_EXPECT_REPOS) { + err = got_error_fmt(GOT_ERR_PRIVSEP_MSG, + "received unexpected imsg %d while in " + "state %d\n", imsg.hdr.type, + writeconf_state); + break; + } + break; + case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_DONE: + if (repo_cur == NULL || + num_notif_refs_needed != 0 || + notif_refs_cur != NULL || + writeconf_state != WRITECONF_STATE_EXPECT_REPOS) { + err = got_error_fmt(GOT_ERR_PRIVSEP_MSG, + "received unexpected imsg %d while in " + "state %d\n", imsg.hdr.type, + writeconf_state); + break; + } + break; + case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_EMAIL: { + struct gotsys_notification_target *target; + + if (repo_cur == NULL || + num_notif_refs_needed != 0 || + notif_refs_cur != NULL || + writeconf_state != WRITECONF_STATE_EXPECT_REPOS) { + err = got_error_fmt(GOT_ERR_PRIVSEP_MSG, + "received unexpected imsg %d while in " + "state %d\n", imsg.hdr.type, + writeconf_state); + break; + } + + err = gotsys_imsg_recv_notification_target_email(NULL, + &target, &imsg); + if (err) + break; + STAILQ_INSERT_TAIL(&repo_cur->notification_targets, + target, entry); + break; + } + case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_HTTP: { + struct gotsys_notification_target *target; + + if (repo_cur == NULL || + num_notif_refs_needed != 0 || + notif_refs_cur != NULL || + writeconf_state != WRITECONF_STATE_EXPECT_REPOS) { + err = got_error_fmt(GOT_ERR_PRIVSEP_MSG, + "received unexpected imsg %d while in " + "state %d\n", imsg.hdr.type, + writeconf_state); + break; + } + + err = gotsys_imsg_recv_notification_target_http(NULL, + &target, &imsg); + if (err) + break; + STAILQ_INSERT_TAIL(&repo_cur->notification_targets, + target, entry); + break; + } + case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGETS_DONE: + break; case GOTSYSD_IMSG_SYSCONF_REPOS_DONE: if (writeconf_state != WRITECONF_STATE_EXPECT_REPOS) { err = got_error_fmt(GOT_ERR_PRIVSEP_MSG, @@ -704,6 +1198,9 @@ dispatch_event(int fd, short event, void *arg) } repo_cur = NULL; writeconf_state = WRITECONF_STATE_WRITE_CONF; + err = prepare_gotd_secrets(); + if (err) + break; err = write_gotd_conf(); if (err) break; @@ -774,6 +1271,10 @@ main(int argc, char *argv[]) GOTD_CONF_PATH, ""); if (err) goto done; + err = got_opentemp_named_fd(&gotd_secrets_tmppath, &gotd_secrets_tmpfd, + GOTD_CONF_PATH, ""); + if (err) + goto done; #ifndef PROFILE if (pledge("stdio rpath wpath cpath fattr chown unveil", NULL) == -1) { err = got_error_from_errno("pledge"); @@ -785,11 +1286,21 @@ main(int argc, char *argv[]) goto done; } + if (unveil(gotd_secrets_tmppath, "rwc") == -1) { + err = got_error_from_errno2("unveil rwc", gotd_secrets_tmppath); + goto done; + } + if (unveil(GOTD_CONF_PATH, "rwc") == -1) { err = got_error_from_errno2("unveil rwc", GOTD_CONF_PATH); goto done; } + if (unveil(GOTD_SECRETS_PATH, "rwc") == -1) { + err = got_error_from_errno2("unveil rwc", GOTD_SECRETS_PATH); + goto done; + } + if (unveil(NULL, NULL) == -1) { err = got_error_from_errno("unveil"); goto done; @@ -811,11 +1322,18 @@ done: if (gotd_conf_tmppath && unlink(gotd_conf_tmppath) == -1 && err == NULL) err = got_error_from_errno2("unlink", gotd_conf_tmppath); free(gotd_conf_tmppath); + if (gotd_secrets_tmppath && unlink(gotd_secrets_tmppath) == -1 && + err == NULL) + err = got_error_from_errno2("unlink", gotd_secrets_tmppath); + free(gotd_secrets_tmppath); if (close(GOTSYSD_FILENO_MSG_PIPE) == -1 && err == NULL) err = got_error_from_errno("close"); if (gotd_conf_tmpfd != -1 && close(gotd_conf_tmpfd) == -1 && err == NULL) err = got_error_from_errno("close"); + if (gotd_secrets_tmpfd != -1 && close(gotd_secrets_tmpfd) == -1 && + err == NULL) + err = got_error_from_errno("close"); if (err) gotsysd_imsg_send_error(&iev.ibuf, 0, 0, err); imsgbuf_clear(&iev.ibuf); blob - 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 @@ -742,11 +742,264 @@ send_protected_refs(struct gotsysd_imsgev *iev, struct if (gotsysd_imsg_compose_event(iev, GOTSYSD_IMSG_SYSCONF_PROTECTED_REFS_DONE, 0, -1, NULL, 0) == -1) return got_error_from_errno("gotsysd_imsg_compose_event"); + + return NULL; +} + +static const struct got_error * +send_notification_target_email(struct gotsysd_imsgev *iev, + const char *repo_name, struct gotsys_notification_target *target) +{ + struct gotsysd_imsg_notitfication_target_email itarget; + struct ibuf *wbuf = NULL; + + memset(&itarget, 0, sizeof(itarget)); + + if (target->conf.email.sender) + itarget.sender_len = strlen(target->conf.email.sender); + if (target->conf.email.recipient) + itarget.recipient_len = strlen(target->conf.email.recipient); + if (target->conf.email.responder) + itarget.responder_len = strlen(target->conf.email.responder); + if (target->conf.email.hostname) + itarget.hostname_len = strlen(target->conf.email.hostname); + if (target->conf.email.port) + itarget.port_len = strlen(target->conf.email.port); + itarget.repo_name_len = strlen(repo_name); + + wbuf = imsg_create(&iev->ibuf, + GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_EMAIL, + 0, 0, sizeof(itarget) + itarget.sender_len + itarget.recipient_len + + itarget.responder_len + itarget.hostname_len + itarget.port_len + + itarget.repo_name_len); + if (wbuf == NULL) { + return got_error_from_errno("imsg_create " + "NOTIFICATION_TARGET_EMAIL"); + } + + if (imsg_add(wbuf, &itarget, sizeof(itarget)) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_EMAIL"); + } + if (target->conf.email.sender) { + if (imsg_add(wbuf, target->conf.email.sender, + itarget.sender_len) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_EMAIL"); + } + } + + if (target->conf.email.recipient) { + if (imsg_add(wbuf, target->conf.email.recipient, + itarget.recipient_len) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_EMAIL"); + } + } + if (target->conf.email.responder) { + if (imsg_add(wbuf, target->conf.email.responder, + itarget.responder_len) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_EMAIL"); + } + } + if (target->conf.email.hostname) { + if (imsg_add(wbuf, target->conf.email.hostname, + itarget.hostname_len) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_EMAIL"); + } + } + if (target->conf.email.port) { + if (imsg_add(wbuf, target->conf.email.port, + itarget.port_len) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_EMAIL"); + } + } + if (imsg_add(wbuf, repo_name, itarget.repo_name_len) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_EMAIL"); + } + imsg_close(&iev->ibuf, wbuf); + gotsysd_imsg_event_add(iev); return NULL; } static const struct got_error * +send_notification_target_http(struct gotsysd_imsgev *iev, const char *repo_name, + struct gotsys_notification_target *target) +{ + struct gotsysd_imsg_notitfication_target_http itarget; + struct ibuf *wbuf = NULL; + + memset(&itarget, 0, sizeof(itarget)); + + itarget.tls = target->conf.http.tls; + itarget.hostname_len = strlen(target->conf.http.hostname); + itarget.port_len = strlen(target->conf.http.port); + itarget.path_len = strlen(target->conf.http.path); + if (target->conf.http.user) + itarget.user_len = strlen(target->conf.http.user); + if (target->conf.http.password) + itarget.password_len = strlen(target->conf.http.password); + if (target->conf.http.hmac_secret) + itarget.hmac_len = strlen(target->conf.http.hmac_secret); + itarget.repo_name_len = strlen(repo_name); + + wbuf = imsg_create(&iev->ibuf, + GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_HTTP, + 0, 0, sizeof(itarget) + itarget.hostname_len + itarget.port_len + + itarget.path_len + itarget.user_len + itarget.password_len + + itarget.hmac_len + itarget.repo_name_len); + if (wbuf == NULL) { + return got_error_from_errno("imsg_create " + "NOTIFICATION_TARGET_HTTP"); + } + + if (imsg_add(wbuf, &itarget, sizeof(itarget)) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_HTTP"); + } + if (imsg_add(wbuf, target->conf.http.hostname, + itarget.hostname_len) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_HTTP"); + } + if (imsg_add(wbuf, target->conf.http.port, + itarget.port_len) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_HTTP"); + } + if (imsg_add(wbuf, target->conf.http.path, + itarget.path_len) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_HTTP"); + } + + if (target->conf.http.user) { + if (imsg_add(wbuf, target->conf.http.user, itarget.user_len) == -1) + return got_error_from_errno("imsg_add NOTIFICATION_TARGET_HTTP"); + } + if (target->conf.http.password) { + if (imsg_add(wbuf, target->conf.http.password, + itarget.password_len) == -1) + return got_error_from_errno("imsg_add NOTIFICATION_TARGET_HTTP"); + } + if (target->conf.http.hmac_secret) { + if (imsg_add(wbuf, target->conf.http.hmac_secret, + itarget.hmac_len) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_HTTP"); + } + } + if (imsg_add(wbuf, repo_name, itarget.repo_name_len) == -1) { + return got_error_from_errno("imsg_add " + "NOTIFICATION_TARGET_HTTP"); + } + + imsg_close(&iev->ibuf, wbuf); + gotsysd_imsg_event_add(iev); + return NULL; +} + +static const struct got_error * +send_notification_target(struct gotsysd_imsgev *iev, const char *repo_name, + struct gotsys_notification_target *target) +{ + const struct got_error *err = NULL; + + switch (target->type) { + case GOTSYS_NOTIFICATION_VIA_EMAIL: + err = send_notification_target_email(iev, repo_name, target); + break; + case GOTSYS_NOTIFICATION_VIA_HTTP: + err = send_notification_target_http(iev, repo_name, target); + break; + default: + break; + } + + return err; +} + +static const struct got_error * +send_notification_targets(struct gotsysd_imsgev *iev, const char *repo_name, + struct gotsys_notification_targets *targets) +{ + const struct got_error *err = NULL; + struct gotsys_notification_target *target; + + STAILQ_FOREACH(target, targets, entry) { + err = send_notification_target(iev, repo_name, target); + if (err) + return err; + } + + if (gotsysd_imsg_compose_event(iev, + GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGETS_DONE, 0, -1, NULL, 0) == -1) + return got_error_from_errno("gotsysd_imsg_compose_event"); + + return NULL; +} + +static const struct got_error * +send_notification_config(struct gotsysd_imsgev *iev, struct gotsys_repo *repo) +{ + const struct got_error *err = NULL; + struct got_pathlist_entry *pe; + struct gotsysd_imsg_pathlist ilist; + + memset(&ilist, 0, sizeof(ilist)); + + ilist.nelem = repo->num_notification_refs; + if (ilist.nelem > 0) { + if (gotsysd_imsg_compose_event(iev, + GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS, 0, -1, + &ilist, sizeof(ilist)) == -1) { + return got_error_from_errno("imsg compose " + "NOTIFICATION_REFS"); + } + + RB_FOREACH(pe, got_pathlist_head, &repo->notification_refs) { + err = send_pathlist_elem(iev, pe->path, + GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_ELEM); + if (err) + return err; + } + } + + if (gotsysd_imsg_compose_event(iev, + GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REFS_DONE, 0, -1, NULL, 0) == -1) + return got_error_from_errno("gotsysd_imsg_compose_event"); + + ilist.nelem = repo->num_notification_ref_namespaces; + if (ilist.nelem > 0) { + if (gotsysd_imsg_compose_event(iev, + GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES, 0, -1, + &ilist, sizeof(ilist)) == -1) { + return got_error_from_errno("imsg compose " + "NOTIFICATION_REF_NAMESPACES"); + } + + RB_FOREACH(pe, got_pathlist_head, + &repo->notification_ref_namespaces) { + err = send_pathlist_elem(iev, pe->path, + GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_ELEM); + if (err) + return err; + } + } + + if (gotsysd_imsg_compose_event(iev, + GOTSYSD_IMSG_SYSCONF_NOTIFICATION_REF_NAMESPACES_DONE, 0, -1, NULL, 0) == -1) + return got_error_from_errno("gotsysd_imsg_compose_event"); + + return send_notification_targets(iev, repo->name, &repo->notification_targets); +} + +static const struct got_error * send_repo(struct gotsysd_imsgev *iev, struct gotsys_repo *repo) { const struct got_error *err; @@ -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 @@ -1878,10 +1878,486 @@ test_override_all_user_access() { return 1 fi done + + # Undo gotsys.conf override + ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} 'rm -f /etc/gotsysd.conf' + + # Restart gotsysd (XXX need a better way to do this...) + ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} 'pkill -xf /usr/local/sbin/gotsysd' + sleep 1 + ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} '/usr/local/sbin/gotsysd -vvv' + sleep 1 + ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} 'gotsys apply -w' > /dev/null test_done "$testroot" "$ret" } +# flan:password encoded in base64 +AUTH="ZmxhbjpwYXNzd29yZA==" + +test_http_notification() { + local testroot=`test_init http_notification 1` + + got checkout -q $testroot/${GOTSYS_REPO} $testroot/wt >/dev/null + ret=$? + if [ $ret -ne 0 ]; then + echo "got checkout failed unexpectedly" >&2 + test_done "$testroot" 1 + return 1 + fi + + crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'` + crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'` + sshkey=`cat ${GOTSYSD_SSH_PUBKEY}` + cat > ${testroot}/wt/gotsys.conf </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;