From: Baptiste Daroussin Date: Tue, 27 Aug 2024 14:33:30 +0000 (+0200) Subject: access: move code handling access into a separate to make it testable X-Git-Tag: RELEASE_1_5_0~19 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d9a7bbdfa42cf5ef5bb838048c73948c87977bf0;p=thirdparty%2Fmlmmj.git access: move code handling access into a separate to make it testable while here, add very basic unit tests --- diff --git a/include/mlmmj.h b/include/mlmmj.h index ca352c92..2aa2a128 100644 --- a/include/mlmmj.h +++ b/include/mlmmj.h @@ -138,6 +138,16 @@ bool send_probe(struct ml *ml, const char *addr); void parse_content_type(strlist *, char **mime_type, char **boundary); char *find_in_list(strlist *, const char *pattern); +typedef enum { + ACT_ALLOW = 0, + ACT_SEND, + ACT_DENY, + ACT_MODERATE, + ACT_DISCARD, +} actions_t; + +actions_t do_access(strlist *rules, strlist *headers, const char *from, int listfd); + #define MY_ASSERT(expression) if (!(expression)) { \ errno = 0; \ log_error(LOG_ARGS, "assertion failed"); \ diff --git a/src/Makefile.am b/src/Makefile.am index 28ce1bd6..86a80845 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -9,7 +9,7 @@ bin_SCRIPTS = mlmmj-make-ml EXTRA_DIST = mlmmj-make-ml -libmlmmj_a_SOURCES= mail-functions.c chomp.c incindexfile.c \ +libmlmmj_a_SOURCES= access.c mail-functions.c chomp.c incindexfile.c \ checkwait_smtpreply.c init_sockfd.c strgen.c \ random-int.c print-version.c log_error.c mygetline.c \ getaddrsfromfile.c readn.c \ diff --git a/src/access.c b/src/access.c new file mode 100644 index 00000000..ad940e2b --- /dev/null +++ b/src/access.c @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2024 Baptiste Daroussin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include +#include + +#include "mlmmj.h" +#include "log_error.h" +#include "log_oper.h" + +static char *action_strs[] = { + "allowed", + "sent", + "denied", + "moderated", + "discarded" +}; + +actions_t +do_access(strlist *rules, strlist *headers, const char *from, int listfd) +{ + unsigned int match; + char *rule_ptr; + char errbuf[128]; + int err; + actions_t act; + unsigned int not; + regex_t regexp; + char *hdr; + size_t i = 0; + + if (rules == NULL) + return (ACT_ALLOW); + + tll_foreach(*rules, it) { + rule_ptr = it->item; + if (strncmp(rule_ptr, "allow", 5) == 0) { + rule_ptr += 5; + act = ACT_ALLOW; + } else if (strncmp(rule_ptr, "send", 4) == 0) { + rule_ptr += 4; + act = ACT_SEND; + } else if (strncmp(rule_ptr, "deny", 4) == 0) { + rule_ptr += 4; + act = ACT_DENY; + } else if (strncmp(rule_ptr, "moderate", 8) == 0) { + rule_ptr += 8; + act = ACT_MODERATE; + } else if (strncmp(rule_ptr, "discard", 7) == 0) { + rule_ptr += 7; + act = ACT_DISCARD; + } else { + errno = 0; + log_error(LOG_ARGS, "Unable to parse rule #%d \"%s\":" + " Missing action keyword. Denying post from \"%s\"", + i, it->item, from); + log_oper(listfd, OPLOGFNAME, "Unable to parse rule #%d \"%s\":" + " Missing action keyword. Denying post from \"%s\"", + i, it->item, from); + return ACT_DENY; + } + + if (*rule_ptr == ' ') { + rule_ptr++; + } else if (*rule_ptr == '\0') { + /* the rule is a keyword and no regexp */ + log_oper(listfd, OPLOGFNAME, "mlmmj-process: access -" + " A mail from \"%s\" was %s by rule #%d \"%s\"", + from, action_strs[act], i, it->item); + return act; + } else { + /* we must have space or end of string */ + errno = 0; + log_error(LOG_ARGS, "Unable to parse rule #%d \"%s\":" + " Invalid character after action keyword." + " Denying post from \"%s\"", i, it->item, from); + log_oper(listfd, OPLOGFNAME, "Unable to parse rule #%d \"%s\":" + " Invalid character after action keyword." + " Denying post from \"%s\"", i, it->item, from); + return ACT_DENY; + } + + if (*rule_ptr == '!') { + rule_ptr++; + not = 1; + } else { + not = 0; + } + + /* remove unanchored ".*" from beginning of regexp to stop the + * regexp matching to loop so long time it seems like it's + * hanging */ + if (strncmp(rule_ptr, "^.*", 3) == 0) { + rule_ptr += 3; + } + while (strncmp(rule_ptr, ".*", 2) == 0) { + rule_ptr += 2; + } + + if ((err = regcomp(®exp, rule_ptr, + REG_EXTENDED | REG_NOSUB | REG_ICASE))) { + regerror(err, ®exp, errbuf, sizeof(errbuf)); + regfree(®exp); + errno = 0; + log_error(LOG_ARGS, "regcomp() failed for rule #%d \"%s\"" + " (message: '%s') (expression: '%s')" + " Denying post from \"%s\"", + i, it->item, errbuf, rule_ptr, from); + log_oper(listfd, OPLOGFNAME, "regcomp() failed for rule" + " #%d \"%s\" (message: '%s') (expression: '%s')" + " Denying post from \"%s\"", + i, it->item, errbuf, rule_ptr, from); + return ACT_DENY; + } + + match = 0; + tll_foreach(*headers, header) { + if (regexec(®exp, header->item, 0, NULL, 0) + == 0) { + match = 1; + hdr = header->item; + break; + } + } + + regfree(®exp); + + if (match != not) { + if (match) { + log_oper(listfd, OPLOGFNAME, "mlmmj-process: access -" + " A mail from \"%s\" with header \"%s\" was %s by" + " rule #%d \"%s\"", from, hdr, action_strs[act], + i, it->item); + } else { + log_oper(listfd, OPLOGFNAME, "mlmmj-process: access -" + " A mail from \"%s\" was %s by rule #%d \"%s\"" + " because no header matched.", from, + action_strs[act], i, it->item); + } + return act; + } + i++; + } + + log_oper(listfd, OPLOGFNAME, "mlmmj-process: access -" + " A mail from \"%s\" didn't match any rules, and" + " was denied by default.", from); + return ACT_DENY; +} + diff --git a/src/mlmmj-process.c b/src/mlmmj-process.c index 301d843d..c6ff3e39 100644 --- a/src/mlmmj-process.c +++ b/src/mlmmj-process.c @@ -56,24 +56,6 @@ #include "send_help.h" #include "send_mail.h" -enum action { - ALLOW, - SEND, - DENY, - MODERATE, - DISCARD -}; - - -static char *action_strs[] = { - "allowed", - "sent", - "denied", - "moderated", - "discarded" -}; - - enum modreason { MODNONSUBPOSTS, MODNONMODPOSTS, @@ -225,135 +207,6 @@ static void newmoderated(struct ml *ml, const char *mailfilename, } -static enum action do_access(strlist *rule_strs, strlist *hdrs, - const char *from, int listfd) -{ - unsigned int match; - char *rule_ptr; - char errbuf[128]; - int err; - enum action act; - unsigned int not; - regex_t regexp; - char *hdr; - size_t i = 0; - - tll_foreach(*rule_strs, it) { - rule_ptr = it->item; - if (strncmp(rule_ptr, "allow", 5) == 0) { - rule_ptr += 5; - act = ALLOW; - } else if (strncmp(rule_ptr, "send", 4) == 0) { - rule_ptr += 4; - act = SEND; - } else if (strncmp(rule_ptr, "deny", 4) == 0) { - rule_ptr += 4; - act = DENY; - } else if (strncmp(rule_ptr, "moderate", 8) == 0) { - rule_ptr += 8; - act = MODERATE; - } else if (strncmp(rule_ptr, "discard", 7) == 0) { - rule_ptr += 7; - act = DISCARD; - } else { - errno = 0; - log_error(LOG_ARGS, "Unable to parse rule #%d \"%s\":" - " Missing action keyword. Denying post from \"%s\"", - i, it->item, from); - log_oper(listfd, OPLOGFNAME, "Unable to parse rule #%d \"%s\":" - " Missing action keyword. Denying post from \"%s\"", - i, it->item, from); - return DENY; - } - - if (*rule_ptr == ' ') { - rule_ptr++; - } else if (*rule_ptr == '\0') { - /* the rule is a keyword and no regexp */ - log_oper(listfd, OPLOGFNAME, "mlmmj-process: access -" - " A mail from \"%s\" was %s by rule #%d \"%s\"", - from, action_strs[act], i, it->item); - return act; - } else { - /* we must have space or end of string */ - errno = 0; - log_error(LOG_ARGS, "Unable to parse rule #%d \"%s\":" - " Invalid character after action keyword." - " Denying post from \"%s\"", i, it->item, from); - log_oper(listfd, OPLOGFNAME, "Unable to parse rule #%d \"%s\":" - " Invalid character after action keyword." - " Denying post from \"%s\"", i, it->item, from); - return DENY; - } - - if (*rule_ptr == '!') { - rule_ptr++; - not = 1; - } else { - not = 0; - } - - /* remove unanchored ".*" from beginning of regexp to stop the - * regexp matching to loop so long time it seems like it's - * hanging */ - if (strncmp(rule_ptr, "^.*", 3) == 0) { - rule_ptr += 3; - } - while (strncmp(rule_ptr, ".*", 2) == 0) { - rule_ptr += 2; - } - - if ((err = regcomp(®exp, rule_ptr, - REG_EXTENDED | REG_NOSUB | REG_ICASE))) { - regerror(err, ®exp, errbuf, sizeof(errbuf)); - regfree(®exp); - errno = 0; - log_error(LOG_ARGS, "regcomp() failed for rule #%d \"%s\"" - " (message: '%s') (expression: '%s')" - " Denying post from \"%s\"", - i, it->item, errbuf, rule_ptr, from); - log_oper(listfd, OPLOGFNAME, "regcomp() failed for rule" - " #%d \"%s\" (message: '%s') (expression: '%s')" - " Denying post from \"%s\"", - i, it->item, errbuf, rule_ptr, from); - return DENY; - } - - match = 0; - tll_foreach(*hdrs, header) { - if (regexec(®exp, header->item, 0, NULL, 0) - == 0) { - match = 1; - hdr = header->item; - break; - } - } - - regfree(®exp); - - if (match != not) { - if (match) { - log_oper(listfd, OPLOGFNAME, "mlmmj-process: access -" - " A mail from \"%s\" with header \"%s\" was %s by" - " rule #%d \"%s\"", from, hdr, action_strs[act], - i, it->item); - } else { - log_oper(listfd, OPLOGFNAME, "mlmmj-process: access -" - " A mail from \"%s\" was %s by rule #%d \"%s\"" - " because no header matched.", from, - action_strs[act], i, it->item); - } - return act; - } - i++; - } - - log_oper(listfd, OPLOGFNAME, "mlmmj-process: access -" - " A mail from \"%s\" didn't match any rules, and" - " was denied by default.", from); - return DENY; -} - static void print_help(const char *prg) { printf("Usage: %s -L /path/to/list\n" @@ -750,12 +603,12 @@ int main(int argc, char **argv) access_rules = ctrlvalues(ml.ctrlfd, "access"); if (access_rules != NULL) { - enum action accret; + actions_t accret; /* Don't send a mail about denial to the list, but silently * discard and exit. Also do this in case it's turned off */ accret = do_access(access_rules, &allheaders, posteraddr, ml.fd); - if (accret == DENY) { + if (accret == ACT_DENY) { if ((strcasecmp(ml.addr, posteraddr) == 0) || statctrl(ml.ctrlfd, "noaccessdenymails")) { log_error(LOG_ARGS, "Discarding %s because" @@ -783,10 +636,10 @@ int main(int argc, char **argv) free(donemailname); free(randomstr); send_help(&ml, queuefilename, posteraddr); - } else if (accret == MODERATE) { + } else if (accret == ACT_MODERATE) { moderated = 1; modreason = ACCESS; - } else if (accret == DISCARD) { + } else if (accret == ACT_DISCARD) { xasprintf(&discardname, "%s/queue/discarded/%s", ml.dir, randomstr); free(randomstr); @@ -800,9 +653,9 @@ int main(int argc, char **argv) free(donemailname); free(discardname); exit(EXIT_SUCCESS); - } else if (accret == SEND) { + } else if (accret == ACT_SEND) { send = 1; - } else if (accret == ALLOW) { + } else if (accret == ACT_ALLOW) { /* continue processing as normal */ } } diff --git a/tests/mlmmj.c b/tests/mlmmj.c index c7c43889..09a4a66d 100644 --- a/tests/mlmmj.c +++ b/tests/mlmmj.c @@ -160,6 +160,7 @@ ATF_TC_WITHOUT_HEAD(mod_get_addr_type); ATF_TC_WITHOUT_HEAD(send_probe); ATF_TC_WITHOUT_HEAD(parse_content_type); ATF_TC_WITHOUT_HEAD(find_in_list); +ATF_TC_WITHOUT_HEAD(do_access); ATF_TC_BODY(random_int, tc) { @@ -2923,6 +2924,27 @@ ATF_TC_BODY(parse_content_type, tc) free(b); } +ATF_TC_BODY(do_access, tc) +{ + int listfd = -1; + strlist rules = tll_init(); + strlist headers = tll_init(); + tll_push_back(rules, "allow"); + + ATF_REQUIRE_EQ(do_access(NULL, NULL, "test@plop", listfd), ACT_ALLOW); + ATF_REQUIRE_EQ(do_access(&rules, NULL, "test@plop", listfd), ACT_ALLOW); + tll_pop_back(rules); + tll_push_back(rules, "deny"); + ATF_REQUIRE_EQ(do_access(&rules, NULL, "test@plop", listfd), ACT_DENY); + tll_pop_back(rules); + tll_push_back(rules, "crap"); + ATF_REQUIRE_EQ(do_access(&rules, NULL, "test@plop", listfd), ACT_DENY); + tll_pop_back(rules); + tll_push_back(headers, "from: toto@bla"); + tll_push_back(rules, "moderate ^from: toto@"); + ATF_REQUIRE_EQ(do_access(&rules, &headers, "test@plop", listfd), ACT_MODERATE); +} + ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, random_int); @@ -3012,6 +3034,7 @@ ATF_TP_ADD_TCS(tp) ATF_TP_ADD_TC(tp, send_probe); ATF_TP_ADD_TC(tp, parse_content_type); ATF_TP_ADD_TC(tp, find_in_list); + ATF_TP_ADD_TC(tp, do_access); return (atf_no_error()); }