diff --git a/postfix/HISTORY b/postfix/HISTORY index fbd6ea037..5f1d4937d 100644 --- a/postfix/HISTORY +++ b/postfix/HISTORY @@ -27491,3 +27491,13 @@ Apologies for any names omitted. src/util/dict_utf8.c, src/util/midna_domain.c, src/util/printable.c, src/util/stringops.h, src/util/valid_utf8_string.c. + + Cleanup: added unit tests to the readlline module, with + multiline input that contains embedded comments, input that + contains a null byte, text not ending in newline. File: + readlline.c. + +20231024 + + Cleanup: emit place holder text when no SASL authentication + failure reason is available. File: smtpd/smtpd_sasl_glue.c. diff --git a/postfix/src/global/mail_version.h b/postfix/src/global/mail_version.h index 1a5567833..94d0b4125 100644 --- a/postfix/src/global/mail_version.h +++ b/postfix/src/global/mail_version.h @@ -20,7 +20,7 @@ * Patches change both the patchlevel and the release date. Snapshots have no * patchlevel; they change the release date only. */ -#define MAIL_RELEASE_DATE "20231012" +#define MAIL_RELEASE_DATE "20231024" #define MAIL_VERSION_NUMBER "3.9" #ifdef SNAPSHOT diff --git a/postfix/src/postscreen/postscreen_smtpd.c b/postfix/src/postscreen/postscreen_smtpd.c index 1e859acc9..6b72626a7 100644 --- a/postfix/src/postscreen/postscreen_smtpd.c +++ b/postfix/src/postscreen/postscreen_smtpd.c @@ -930,7 +930,9 @@ static void psc_smtpd_read_event(int event, void *context) } /* - * Avoid complaints from Postfix maps about malformed content. + * Avoid complaints from Postfix maps about malformed content. Note: + * this will stop at the first null byte, just like the code that + * parses the command name or command arguments. */ #define PSC_BAD_UTF8(str) \ (var_smtputf8_enable && !valid_utf8_stringz(str)) diff --git a/postfix/src/smtpd/smtpd_sasl_glue.c b/postfix/src/smtpd/smtpd_sasl_glue.c index 6586e005b..7103d4523 100644 --- a/postfix/src/smtpd/smtpd_sasl_glue.c +++ b/postfix/src/smtpd/smtpd_sasl_glue.c @@ -346,8 +346,8 @@ int smtpd_sasl_authenticate(SMTPD_STATE *state, if (status != XSASL_AUTH_DONE) { sasl_username = xsasl_server_get_username(state->sasl_server); msg_warn("%s: SASL %s authentication failed: %s, sasl_username=%s", - state->namaddr, sasl_method, - STR(state->sasl_reply), + state->namaddr, sasl_method, *STR(state->sasl_reply) ? + STR(state->sasl_reply) : "(reason unavailable)", sasl_username ? sasl_username : "(unavailable)"); /* RFC 4954 Section 6. */ if (status == XSASL_AUTH_TEMP) diff --git a/postfix/src/util/Makefile.in b/postfix/src/util/Makefile.in index 43894246e..f4c8e6e58 100644 --- a/postfix/src/util/Makefile.in +++ b/postfix/src/util/Makefile.in @@ -145,7 +145,7 @@ TESTPROG= dict_open dup2_pass_on_exec events exec_command fifo_open \ vstream timecmp dict_cache midna_domain casefold strcasecmp_utf8 \ vbuf_print split_qnameval vstream msg_logger byte_mask \ known_tcp_ports dict_stream find_inet binhash hash_fnv argv \ - clean_env inet_prefix_top printable + clean_env inet_prefix_top printable readlline PLUGIN_MAP_SO = $(LIB_PREFIX)pcre$(LIB_SUFFIX) $(LIB_PREFIX)lmdb$(LIB_SUFFIX) \ $(LIB_PREFIX)cdb$(LIB_SUFFIX) $(LIB_PREFIX)sdbm$(LIB_SUFFIX) HTABLE_FIX = NORANDOMIZE=1 @@ -370,6 +370,11 @@ printable: $(LIB) $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(SYSLIBS) mv junk $@.o +readlline: $(LIB) + mv $@.o junk + $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(SYSLIBS) + mv junk $@.o + hex_quote: $(LIB) mv $@.o junk $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(SYSLIBS) @@ -624,7 +629,7 @@ tests: all valid_hostname_test mac_expand_test dict_test unescape_test \ miss_endif_regexp_test split_qnameval_test vstring_test \ vstream_test byte_mask_tests mystrtok_test known_tcp_ports_test \ binhash_test argv_test inet_prefix_top_test printable_test \ - valid_utf8_string_test + valid_utf8_string_test readlline_test dict_tests: all dict_test \ dict_pcre_tests dict_cidr_test dict_thash_test dict_static_test \ @@ -659,6 +664,9 @@ unescape_test: unescape unescape.in unescape.ref printable_test: printable $(SHLIB_ENV) ${VALGRIND} ./printable +readlline_test: readlline + $(SHLIB_ENV) ${VALGRIND} ./readlline + valid_utf8_string_test: valid_utf8_string $(SHLIB_ENV) ${VALGRIND} ./valid_utf8_string diff --git a/postfix/src/util/printable.c b/postfix/src/util/printable.c index b944daa13..0e1ae1954 100644 --- a/postfix/src/util/printable.c +++ b/postfix/src/util/printable.c @@ -190,6 +190,7 @@ int main(int argc, char **argv) for (pass = fail = 0, tp = testcases; tp < testcases + NUM_TESTS; tp++) { char *input; char *actual; + int ok = 0; /* * Notes: @@ -206,13 +207,17 @@ int main(int argc, char **argv) if (strcmp(actual, tp->expected) != 0) { vstream_fprintf(VSTREAM_ERR, "input: >%s<, got: >%s<, want: >%s<\n", tp->input, actual, tp->expected); - vstream_fprintf(VSTREAM_ERR, "FAIL %s\n", tp->name); - fail++; } else { vstream_fprintf(VSTREAM_ERR, "input: >%s<, got and want: >%s<\n", tp->input, actual); + ok = 1; + } + if (ok) { vstream_fprintf(VSTREAM_ERR, "PASS %s\n", tp->name); pass++; + } else { + vstream_fprintf(VSTREAM_ERR, "FAIL %s\n", tp->name); + fail++; } myfree(input); } diff --git a/postfix/src/util/readlline.c b/postfix/src/util/readlline.c index 015877a2f..721b75f22 100644 --- a/postfix/src/util/readlline.c +++ b/postfix/src/util/readlline.c @@ -85,9 +85,15 @@ VSTRING *readllines(VSTRING *buf, VSTREAM *fp, int *lineno, int *first_line) int next; ssize_t start; char *cp; + int my_lineno = 0, my_first_line, got_null = 0; VSTRING_RESET(buf); + if (lineno == 0) + lineno = &my_lineno; + if (first_line == 0) + first_line = &my_first_line; + /* * Ignore comment lines, all whitespace lines, and empty lines. Terminate * at EOF or at the beginning of the next logical line. @@ -95,16 +101,19 @@ VSTRING *readllines(VSTRING *buf, VSTREAM *fp, int *lineno, int *first_line) for (;;) { /* Read one line, possibly not newline terminated. */ start = LEN(buf); - while ((ch = VSTREAM_GETC(fp)) != VSTREAM_EOF && ch != '\n') + while ((ch = VSTREAM_GETC(fp)) != VSTREAM_EOF && ch != '\n') { VSTRING_ADDCH(buf, ch); - if (lineno != 0 && (ch == '\n' || LEN(buf) > start)) + if (ch == 0) + got_null = 1; + } + if (ch == '\n' || LEN(buf) > start) *lineno += 1; /* Ignore comment line, all whitespace line, or empty line. */ for (cp = STR(buf) + start; cp < END(buf) && ISSPACE(*cp); cp++) /* void */ ; if (cp == END(buf) || *cp == '#') vstring_truncate(buf, start); - else if (start == 0 && lineno != 0 && first_line != 0) + if (start == 0) *first_line = *lineno; /* Terminate at EOF or at the beginning of the next logical line. */ if (ch == VSTREAM_EOF) @@ -118,6 +127,20 @@ VSTRING *readllines(VSTRING *buf, VSTREAM *fp, int *lineno, int *first_line) } VSTRING_TERMINATE(buf); + /* + * This code does not care about embedded null bytes, but callers do. + */ + if (got_null) { + const char *why = "text after null byte may be ignored"; + + if (*first_line == *lineno) + msg_warn("%s, line %d: %s", + VSTREAM_PATH(fp), *lineno, why); + else + msg_warn("%s, line %d-%d: %s", + VSTREAM_PATH(fp), *first_line, *lineno, why); + } + /* * Invalid input: continuing text without preceding text. Allowing this * would complicate "postconf -e", which implements its own multi-line @@ -136,3 +159,205 @@ VSTRING *readllines(VSTRING *buf, VSTREAM *fp, int *lineno, int *first_line) */ return (LEN(buf) > 0 ? buf : 0); } + + /* + * Stand-alone test program. + */ +#ifdef TEST +#include +#include +#include +#include +#include +#include +#include + + /* + * Test cases. Note: the input and exp_output fields are converted with + * unescape(). Embedded null bytes must be specified as \\0. + */ +struct testcase { + const char *name; + const char *input; + const char *exp_output; + int exp_first_line; + int exp_last_line; +}; + +static const struct testcase testcases[] = { + {"leading space before non-comment", + " abcde\nfghij\n", + "fghij", + 2, 2 + /* Expect "logical line must not start with whitespace" */ + }, + {"leading space before leading comment", + " #abcde\nfghij\n", + "fghij", + 2, 2 + }, + {"leading #comment at beginning of line", + "#abc\ndef", + "def", + 2, 2, + }, + {"empty line before non-comment", + "\nabc\n", + "abc", + 2, 2, + }, + {"whitespace line before non-comment", + " \nabc\n", + "abc", + 2, 2, + }, + {"missing newline at end of non-comment", + "abc def", + "abc def", + 1, 1, + }, + {"missing newline at end of comment", + "#abc def", + "", + 1, 1, + }, + {"embedded null, single-line", + "abc\\0def", + "abc\\0def", + 1, 1, + /* Expect "line 1: text after null byte may be ignored" */ + }, + {"embedded null, multiline", + "abc\\0\n def", + "abc\\0 def", + 1, 2, + /* Expect "line 1-2: text after null byte may be ignored" */ + }, + {"embedded null in comment", + "#abc\\0\ndef", + "def", + 2, 2, + /* Expect "line 2: text after null byte may be ignored" */ + }, + {"multiline input", + "abc\n def\n", + "abc def", + 1, 2, + }, + {"multiline input with embedded #comment after space", + "abc\n #def\n ghi", + "abc ghi", + 1, 3, + }, + {"multiline input with embedded #comment flush left", + "abc\n#def\n ghi", + "abc ghi", + 1, 3, + }, + {"multiline input with embedded whitespace line", + "abc\n \n ghi", + "abc ghi", + 1, 3, + }, + {"multiline input with embedded empty line", + "abc\n\n ghi", + "abc ghi", + 1, 3, + }, + {"multiline input with embedded #comment after space", + "abc\n #def\n", + "abc", + 1, 2, + }, + {"multiline input with embedded #comment flush left", + "abc\n#def\n", + "abc", + 1, 2, + }, + {"empty line at end of file", + "\n", + "", + 1, 1, + }, + {"whitespace line at end of file", + "\n \n", + "", + 2, 2, + }, + {"whitespace at end of file", + "abc\n ", + "abc", + 1, 2, + }, +}; + +int main(int argc, char **argv) +{ + const struct testcase *tp; + VSTRING *inp_buf = vstring_alloc(100); + VSTRING *exp_buf = vstring_alloc(100); + VSTRING *out_buf = vstring_alloc(100); + VSTRING *esc_buf = vstring_alloc(100); + VSTREAM *fp; + int last_line; + int first_line; + int pass; + int fail; + +#define NUM_TESTS sizeof(testcases)/sizeof(testcases[0]) + + msg_vstream_init(basename(argv[0]), VSTREAM_ERR); + util_utf8_enable = 1; + + for (pass = fail = 0, tp = testcases; tp < testcases + NUM_TESTS; tp++) { + int ok = 0; + + vstream_fprintf(VSTREAM_ERR, "RUN %s\n", tp->name); + unescape(inp_buf, tp->input); + unescape(exp_buf, tp->exp_output); + if ((fp = vstream_memopen(inp_buf, O_RDONLY)) == 0) + msg_panic("open memory stream for reading: %m"); + vstream_control(fp, CA_VSTREAM_CTL_PATH("memory buffer"), + CA_VSTREAM_CTL_END); + last_line = 0; + if (readllines(out_buf, fp, &last_line, &first_line) == 0) { + VSTRING_RESET(out_buf); + VSTRING_TERMINATE(out_buf); + } + if (LEN(out_buf) != LEN(exp_buf)) { + msg_warn("unexpected output length, got: %ld, want: %ld", + (long) LEN(out_buf), (long) LEN(exp_buf)); + } else if (memcmp(STR(out_buf), STR(exp_buf), LEN(out_buf)) != 0) { + msg_warn("unexpected output: got: >%s<, want: >%s<", + STR(escape(esc_buf, STR(out_buf), LEN(out_buf))), + tp->exp_output); + } else if (first_line != tp->exp_first_line) { + msg_warn("unexpected first_line: got: %d, want: %d", + first_line, tp->exp_first_line); + } else if (last_line != tp->exp_last_line) { + msg_warn("unexpected last_line: got: %d, want: %d", + last_line, tp->exp_last_line); + } else { + vstream_fprintf(VSTREAM_ERR, "got and want: >%s<\n", + tp->exp_output); + ok = 1; + } + if (ok) { + vstream_fprintf(VSTREAM_ERR, "PASS %s\n", tp->name); + pass++; + } else { + vstream_fprintf(VSTREAM_ERR, "FAIL %s\n", tp->name); + fail++; + } + vstream_fclose(fp); + } + vstring_free(inp_buf); + vstring_free(exp_buf); + vstring_free(out_buf); + vstring_free(esc_buf); + + msg_info("PASS=%d FAIL=%d", pass, fail); + return (fail > 0); +} + +#endif diff --git a/postfix/src/util/valid_utf8_string.c b/postfix/src/util/valid_utf8_string.c index 4cea0219b..f5b4ff448 100644 --- a/postfix/src/util/valid_utf8_string.c +++ b/postfix/src/util/valid_utf8_string.c @@ -198,6 +198,7 @@ int main(int argc, char **argv) for (pass = fail = 0, tp = testcases; tp < testcases + NUM_TESTS; tp++) { int actual_l; int actual_z; + int ok = 0; /* * Notes: @@ -214,20 +215,22 @@ int main(int argc, char **argv) "input: >%s<, 'actual_l' got: >%s<, want: >%s<\n", tp->input, valid_to_str(actual_l), valid_to_str(tp->expected)); - vstream_fprintf(VSTREAM_ERR, "FAIL %s\n", tp->name); - fail++; } else if (actual_z != tp->expected) { vstream_fprintf(VSTREAM_ERR, "input: >%s<, 'actual_z' got: >%s<, want: >%s<\n", tp->input, valid_to_str(actual_z), valid_to_str(tp->expected)); - vstream_fprintf(VSTREAM_ERR, "FAIL %s\n", tp->name); - fail++; } else { vstream_fprintf(VSTREAM_ERR, "input: >%s<, got and want: >%s<\n", tp->input, valid_to_str(actual_l)); + ok = 1; + } + if (ok) { vstream_fprintf(VSTREAM_ERR, "PASS %s\n", tp->name); pass++; + } else { + vstream_fprintf(VSTREAM_ERR, "FAIL %s\n", tp->name); + fail++; } } msg_info("PASS=%d FAIL=%d", pass, fail);