commit 05a7a31cc5fec292b1ca3e6298afb82029372a79 from: Stefan Sperling date: Mon Mar 11 09:49:08 2024 UTC parse notification configuration commit - c2b405718eabe04c7ac3fa5916c1951227ab99fe commit + 05a7a31cc5fec292b1ca3e6298afb82029372a79 blob - bd50b7baeef6f944713df1b7a9cd66e274d6b537 blob + 8b09c32c7483d4e3129c291aa428fc5e16a84e47 --- gotd/gotd.h +++ gotd/gotd.h @@ -87,6 +87,31 @@ struct gotd_access_rule { char *identifier; }; STAILQ_HEAD(gotd_access_rule_list, gotd_access_rule); + +enum gotd_notification_target_type { + GOTD_NOTIFICATION_VIA_EMAIL, + GOTD_NOTIFICATION_VIA_HTTP +}; + +struct gotd_notification_target { + STAILQ_ENTRY(gotd_notification_target) entry; + + enum gotd_notification_target_type type; + union { + struct { + char *recipient; + int with_diff; + int shortlog; + } email; + struct { + char *url; + int shortlog; + char *user; + char *password; + } http; + } conf; +}; +STAILQ_HEAD(gotd_notification_targets, gotd_notification_target); struct gotd_repo { TAILQ_ENTRY(gotd_repo) entry; @@ -98,6 +123,11 @@ struct gotd_repo { struct got_pathlist_head protected_tag_namespaces; struct got_pathlist_head protected_branch_namespaces; struct got_pathlist_head protected_branches; + + struct got_pathlist_head notification_refs; + struct got_pathlist_head notification_ref_namespaces; + int summarize_notifications; + struct gotd_notification_targets notification_targets; }; TAILQ_HEAD(gotd_repolist, gotd_repo); @@ -470,6 +500,8 @@ struct gotd_repo *gotd_find_repo_by_path(const char *, struct gotd_uid_connection_limit *gotd_find_uid_connection_limit( struct gotd_uid_connection_limit *limits, size_t nlimits, uid_t uid); int gotd_parseuid(const char *s, uid_t *uid); +const struct got_error *gotd_parse_url(char **, char **, char **, + char **, const char *); /* imsg.c */ const struct got_error *gotd_imsg_flush(struct imsgbuf *); blob - 58db764d105aff68e65237083482558bcf17fb47 blob + 58651f6173da0ee1a7f94cb40e58aee574f62939 --- gotd/parse.y +++ gotd/parse.y @@ -102,6 +102,15 @@ static int conf_protect_branch_namespace( struct gotd_repo *, char *); static int conf_protect_branch(struct gotd_repo *, char *); +static int conf_notify_branch(struct gotd_repo *, + char *); +static int conf_notify_ref_namespace(struct gotd_repo *, + char *); +static void conf_notify_summarize(struct gotd_repo *); +static int conf_notify_email(struct gotd_repo *, + char *, int, int); +static int conf_notify_http(struct gotd_repo *, + char *, int, char *, char *); static enum gotd_procid gotd_proc_id; typedef struct { @@ -117,7 +126,8 @@ typedef struct { %token PATH ERROR LISTEN ON USER REPOSITORY PERMIT DENY %token RO RW CONNECTION LIMIT REQUEST TIMEOUT -%token PROTECT NAMESPACE BRANCH TAG +%token PROTECT NAMESPACE BRANCH TAG REFERENCE +%token NOTIFY SUMMARIZE SHORTLOG EMAIL URL PASSWORD WITH DIFF %token STRING %token NUMBER @@ -299,6 +309,130 @@ protectflags : TAG NAMESPACE STRING { } ; +notify : NOTIFY '{' optnl notifyflags_l '}' + | NOTIFY notifyflags + +notifyflags_l : notifyflags optnl notifyflags_l + | notifyflags optnl + ; + +notifyflags : BRANCH STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE) { + if (conf_notify_branch(new_repo, $2)) { + free($2); + YYERROR; + } + free($2); + } + } + | REFERENCE NAMESPACE STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE) { + if (conf_notify_ref_namespace(new_repo, $3)) { + free($3); + YYERROR; + } + free($3); + } + } + | SUMMARIZE { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE) + conf_notify_summarize(new_repo); + } + | EMAIL STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE) { + if (conf_notify_email(new_repo, $2, 0, 0)) { + free($2); + YYERROR; + } + free($2); + } + } + | EMAIL STRING SHORTLOG { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE) { + if (conf_notify_email(new_repo, $2, 1, 0)) { + free($2); + YYERROR; + } + free($2); + } + } + | EMAIL STRING WITH DIFF { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE) { + if (conf_notify_email(new_repo, $2, 0, 1)) { + free($2); + YYERROR; + } + free($2); + } + } + | EMAIL STRING SHORTLOG WITH DIFF { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE) { + if (conf_notify_email(new_repo, $2, 1, 1)) { + free($2); + YYERROR; + } + free($2); + } + } + | URL STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE) { + if (conf_notify_http(new_repo, $2, 0, NULL, + NULL)) { + free($2); + YYERROR; + } + free($2); + } + } + | URL STRING SHORTLOG { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE) { + if (conf_notify_http(new_repo, $2, 1, NULL, + NULL)) { + free($2); + YYERROR; + } + free($2); + } + } + | URL STRING USER STRING PASSWORD STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE) { + if (conf_notify_http(new_repo, $2, 1, $4, $6)) { + free($2); + free($4); + free($6); + YYERROR; + } + free($2); + free($4); + free($6); + } + } + | URL STRING SHORTLOG USER STRING PASSWORD STRING { + if (gotd_proc_id == PROC_GOTD || + gotd_proc_id == PROC_SESSION_WRITE) { + if (conf_notify_http(new_repo, $2, 1, $5, $7)) { + free($2); + free($5); + free($7); + YYERROR; + } + free($2); + free($5); + free($7); + } + } + ; + repository : REPOSITORY STRING { struct gotd_repo *repo; @@ -385,6 +519,7 @@ repoopts1 : PATH STRING { free($2); } | protect + | notify ; repoopts2 : repoopts2 repoopts1 nl @@ -435,20 +570,28 @@ lookup(char *s) { "branch", BRANCH }, { "connection", CONNECTION }, { "deny", DENY }, + { "diff", DIFF }, + { "email", EMAIL }, { "limit", LIMIT }, { "listen", LISTEN }, { "namespace", NAMESPACE }, { "on", ON }, + { "password", PASSWORD }, { "path", PATH }, { "permit", PERMIT }, { "protect", PROTECT }, + { "reference", REFERENCE }, { "repository", REPOSITORY }, { "request", REQUEST }, { "ro", RO }, { "rw", RW }, + { "shortlog", SHORTLOG }, + { "summarize", SUMMARIZE }, { "tag", TAG }, { "timeout", TIMEOUT }, + { "url", URL }, { "user", USER }, + { "with", WITH } }; const struct keywords *p; @@ -918,7 +1061,11 @@ conf_new_repo(const char *name) STAILQ_INIT(&repo->rules); TAILQ_INIT(&repo->protected_tag_namespaces); TAILQ_INIT(&repo->protected_branch_namespaces); + TAILQ_INIT(&repo->protected_branches); TAILQ_INIT(&repo->protected_branches); + TAILQ_INIT(&repo->notification_refs); + TAILQ_INIT(&repo->notification_ref_namespaces); + STAILQ_INIT(&repo->notification_targets); if (strlcpy(repo->name, name, sizeof(repo->name)) >= sizeof(repo->name)) @@ -1069,12 +1216,183 @@ conf_protect_branch(struct gotd_repo *repo, char *bran yyerror("got_pathlist_insert: %s", error->msg); else yyerror("duplicate protect branch %s", branchname); + return -1; + } + + return 0; +} + +static int +conf_notify_branch(struct gotd_repo *repo, char *branchname) +{ + const struct got_error *error; + struct got_pathlist_entry *pe; + char *refname; + + if (strncmp(branchname, "refs/heads/", 11) != 0) { + if (asprintf(&refname, "refs/heads/%s", branchname) == -1) { + yyerror("asprintf: %s", strerror(errno)); + return -1; + } + } else { + refname = strdup(branchname); + if (refname == NULL) { + yyerror("strdup: %s", strerror(errno)); + return -1; + } + } + + if (!refname_is_valid(refname)) { + free(refname); + return -1; + } + + error = got_pathlist_insert(&pe, &repo->notification_refs, + refname, NULL); + if (error) { + free(refname); + yyerror("got_pathlist_insert: %s", error->msg); + return -1; + } + if (pe == NULL) + free(refname); + + return 0; +} + +static int +conf_notify_ref_namespace(struct gotd_repo *repo, char *namespace) +{ + const struct got_error *error; + struct got_pathlist_entry *pe; + char *s; + + got_path_strip_trailing_slashes(namespace); + if (!refname_is_valid(namespace)) + return -1; + + if (asprintf(&s, "%s/", namespace) == -1) { + yyerror("asprintf: %s", strerror(errno)); return -1; } + error = got_pathlist_insert(&pe, &repo->notification_ref_namespaces, + s, NULL); + if (error) { + free(s); + yyerror("got_pathlist_insert: %s", error->msg); + return -1; + } + if (pe == NULL) + free(s); + return 0; } +static void +conf_notify_summarize(struct gotd_repo *repo) +{ + repo->summarize_notifications = 1; +} + +static int +conf_notify_email(struct gotd_repo *repo, char *recipient, int shortlog, + int with_diff) +{ + struct gotd_notification_target *target; + + STAILQ_FOREACH(target, &repo->notification_targets, entry) { + if (target->type != GOTD_NOTIFICATION_VIA_EMAIL) + continue; + if (strcmp(target->conf.email.recipient, recipient) == 0) { + yyerror("duplicate email notification for '%s' in " + "repository '%s'", recipient, repo->name); + return -1; + } + } + + target = calloc(1, sizeof(*target)); + if (target == NULL) + fatal("calloc"); + target->type = GOTD_NOTIFICATION_VIA_EMAIL; + target->conf.email.recipient = strdup(recipient); + if (target->conf.email.recipient == NULL) + fatal("calloc"); + target->conf.email.with_diff = with_diff; + target->conf.email.shortlog = shortlog; + + STAILQ_INSERT_TAIL(&repo->notification_targets, target, entry); + return 0; +} + +static int +conf_notify_http(struct gotd_repo *repo, char *url, int shortlog, + char *user, char *password) +{ + const struct got_error *error; + struct gotd_notification_target *target; + char *proto, *host, *port, *request_path; + int ret = 0; + + error = gotd_parse_url(&proto, &host, &port, &request_path, url); + if (error) { + yyerror("invalid HTTP notification URL '%s' in " + "repository '%s': %s", url, repo->name, error->msg); + return -1; + } + + if (strcmp(proto, "http") != 0 && strcmp(proto, "https") != 0) { + yyerror("invalid protocol '%s' in notification URL '%s' in " + "repository '%s", proto, url, repo->name); + ret = -1; + goto done; + } + + if (strcmp(proto, "http") == 0 && (user != NULL || password != NULL)) { + log_warnx("%s: WARNING: Using basic authentication over " + "plaintext http:// will leak credentials; https:// is " + "recommended for URL '%s'", getprogname(), url); + } + + STAILQ_FOREACH(target, &repo->notification_targets, entry) { + if (target->type != GOTD_NOTIFICATION_VIA_HTTP) + continue; + if (strcmp(target->conf.http.url, url) == 0) { + yyerror("duplicate notification for URL '%s' in " + "repository '%s'", url, repo->name); + ret = -1; + goto done; + } + } + + target = calloc(1, sizeof(*target)); + if (target == NULL) + fatal("calloc"); + target->type = GOTD_NOTIFICATION_VIA_HTTP; + target->conf.http.url = strdup(url); + if (target->conf.http.url == NULL) + fatal("calloc"); + target->conf.http.shortlog = shortlog; + if (user) { + target->conf.http.user = strdup(user); + if (target->conf.http.user == NULL) + fatal("calloc"); + } + if (password) { + target->conf.http.password = strdup(password); + if (target->conf.http.password == NULL) + fatal("calloc"); + } + + STAILQ_INSERT_TAIL(&repo->notification_targets, target, entry); +done: + free(proto); + free(host); + free(port); + free(request_path); + return ret; +} + int symset(const char *nam, const char *val, int persist) { @@ -1198,3 +1516,87 @@ gotd_parseuid(const char *s, uid_t *uid) return -1; return 0; } + +const struct got_error * +gotd_parse_url(char **proto, char **host, char **port, + char **request_path, const char *url) +{ + const struct got_error *err = NULL; + char *s, *p, *q; + + *proto = *host = *port = *request_path = NULL; + + p = strstr(url, "://"); + if (!p) + return got_error(GOT_ERR_PARSE_URI); + + *proto = strndup(url, p - url); + if (*proto == NULL) { + err = got_error_from_errno("strndup"); + goto done; + } + s = p + 3; + + p = strstr(s, "/"); + if (p == NULL || strlen(p) == 1) { + err = got_error(GOT_ERR_PARSE_URI); + goto done; + } + + q = memchr(s, ':', p - s); + if (q) { + *host = strndup(s, q - s); + if (*host == NULL) { + err = got_error_from_errno("strndup"); + goto done; + } + if ((*host)[0] == '\0') { + err = got_error(GOT_ERR_PARSE_URI); + goto done; + } + *port = strndup(q + 1, p - (q + 1)); + if (*port == NULL) { + err = got_error_from_errno("strndup"); + goto done; + } + if ((*port)[0] == '\0') { + err = got_error(GOT_ERR_PARSE_URI); + goto done; + } + } else { + *host = strndup(s, p - s); + if (*host == NULL) { + err = got_error_from_errno("strndup"); + goto done; + } + if ((*host)[0] == '\0') { + err = got_error(GOT_ERR_PARSE_URI); + goto done; + } + } + + while (p[0] == '/' && p[1] == '/') + p++; + *request_path = strdup(p); + if (*request_path == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + got_path_strip_trailing_slashes(*request_path); + if ((*request_path)[0] == '\0') { + err = got_error(GOT_ERR_PARSE_URI); + goto done; + } +done: + if (err) { + free(*proto); + *proto = NULL; + free(*host); + *host = NULL; + free(*port); + *port = NULL; + free(*request_path); + *request_path = NULL; + } + return err; +}