diff options
Diffstat (limited to 'src')
141 files changed, 4042 insertions, 788 deletions
diff --git a/src/bounce/bounce_notify_util.c b/src/bounce/bounce_notify_util.c index 781a525..2482359 100644 --- a/src/bounce/bounce_notify_util.c +++ b/src/bounce/bounce_notify_util.c @@ -773,15 +773,14 @@ int bounce_header_dsn(VSTREAM *bounce, BOUNCE_INFO *bounce_info) post_mail_fprintf(bounce, "X-%s-Queue-ID: %s", bounce_info->mail_name, bounce_info->queue_id); -#define IS_UTF8_ADDRESS(str, len) \ - ((str)[0] != 0 && !allascii(str) && valid_utf8_string((str), (len))) +#define IS_UTF8_ADDRESS(str) \ + ((str)[0] != 0 && !allascii(str) && valid_utf8_stringz(str)) /* Fix 20140708: use "utf-8" or "rfc822" as appropriate. */ if (VSTRING_LEN(bounce_info->sender) > 0) post_mail_fprintf(bounce, "X-%s-Sender: %s; %s", bounce_info->mail_name, bounce_info->smtputf8 - && IS_UTF8_ADDRESS(STR(bounce_info->sender), - VSTRING_LEN(bounce_info->sender)) ? + && IS_UTF8_ADDRESS(STR(bounce_info->sender)) ? "utf-8" : "rfc822", STR(bounce_info->sender)); if (bounce_info->arrival_time > 0) post_mail_fprintf(bounce, "Arrival-Date: %s", @@ -800,8 +799,7 @@ int bounce_recipient_dsn(VSTREAM *bounce, BOUNCE_INFO *bounce_info) /* Fix 20140708: Don't send "utf-8" type with non-UTF8 address. */ post_mail_fprintf(bounce, "Final-Recipient: %s; %s", bounce_info->smtputf8 - && IS_UTF8_ADDRESS(rcpt->address, - strlen(rcpt->address)) ? + && IS_UTF8_ADDRESS(rcpt->address) ? "utf-8" : "rfc822", rcpt->address); /* @@ -829,8 +827,7 @@ int bounce_recipient_dsn(VSTREAM *bounce, BOUNCE_INFO *bounce_info) /* Fix 20140708: Don't send "utf-8" type with non-UTF8 address. */ post_mail_fprintf(bounce, "Original-Recipient: %s; %s", bounce_info->smtputf8 - && IS_UTF8_ADDRESS(rcpt->orig_addr, - strlen(rcpt->orig_addr)) ? + && IS_UTF8_ADDRESS(rcpt->orig_addr) ? "utf-8" : "rfc822", rcpt->orig_addr); } post_mail_fprintf(bounce, "Action: %s", diff --git a/src/bounce/with-msgid-with-filter-no-thread.ref b/src/bounce/with-msgid-with-filter-no-thread.ref index fa30ddf..adcd507 100644 --- a/src/bounce/with-msgid-with-filter-no-thread.ref +++ b/src/bounce/with-msgid-with-filter-no-thread.ref @@ -24,7 +24,7 @@ Reporting-MTA: dns; mail.example Original-Envelope-Id: TEST-ENVID X-Postfix-Queue-ID: msgid X-Postfix-Sender: rfc822; sender@sender.example -Arrival-Date: Sat, 5 Dec 2020 13:31:48 -0500 (EST) +Arrival-Date: Sat, 05 Dec 2020 13:31:48 -0500 (EST) Final-Recipient: rfc822; rcpt-address Original-Recipient: rfc822; rcpt-orig_addr diff --git a/src/bounce/with-msgid-with-filter-with-thread.ref b/src/bounce/with-msgid-with-filter-with-thread.ref index 14d3373..ec3de87 100644 --- a/src/bounce/with-msgid-with-filter-with-thread.ref +++ b/src/bounce/with-msgid-with-filter-with-thread.ref @@ -26,7 +26,7 @@ Reporting-MTA: dns; mail.example Original-Envelope-Id: TEST-ENVID X-Postfix-Queue-ID: msgid X-Postfix-Sender: rfc822; sender@sender.example -Arrival-Date: Sat, 5 Dec 2020 13:31:48 -0500 (EST) +Arrival-Date: Sat, 05 Dec 2020 13:31:48 -0500 (EST) Final-Recipient: rfc822; rcpt-address Original-Recipient: rfc822; rcpt-orig_addr diff --git a/src/cleanup/Makefile.in b/src/cleanup/Makefile.in index 8a3c18c..b74fe0a 100644 --- a/src/cleanup/Makefile.in +++ b/src/cleanup/Makefile.in @@ -88,7 +88,9 @@ milter_tests: cleanup_milter_test bug_tests \ cleanup_milter_test15g cleanup_milter_test15h cleanup_milter_test15i \ cleanup_milter_test16a cleanup_milter_test16b cleanup_milter_test17a \ cleanup_milter_test17b cleanup_milter_test17c cleanup_milter_test17d \ - cleanup_milter_test17e cleanup_milter_test17f cleanup_milter_test17g + cleanup_milter_test17e cleanup_milter_test17f cleanup_milter_test17g \ + cleanup_milter_test18a cleanup_milter_test18b cleanup_milter_test18c \ + cleanup_milter_test18d root_tests: @@ -671,6 +673,46 @@ cleanup_milter_test17g: cleanup_milter test-queue-file17 cleanup_milter.in17g \ diff cleanup_milter.ref17g2 cleanup_milter.tmp2 rm -f test-queue-file17g.tmp cleanup_milter.tmp1 cleanup_milter.tmp2 +cleanup_milter_test18a: cleanup_milter test-queue-file18 cleanup_milter.in18a \ + cleanup_milter.ref18a1 ../postcat/postcat cleanup_milter.ref18a2 + cp test-queue-file18 test-queue-file18a.tmp + chmod u+w test-queue-file18a.tmp + $(SHLIB_ENV) $(VALGRIND) ./cleanup_milter <cleanup_milter.in18a 2>cleanup_milter.tmp1 + diff cleanup_milter.ref18a1 cleanup_milter.tmp1 + $(SHLIB_ENV) $(VALGRIND) ../postcat/postcat -ov test-queue-file18a.tmp 2>/dev/null >cleanup_milter.tmp2 + diff cleanup_milter.ref18a2 cleanup_milter.tmp2 + rm -f test-queue-file18a.tmp cleanup_milter.tmp1 cleanup_milter.tmp2 + +cleanup_milter_test18b: cleanup_milter test-queue-file18 cleanup_milter.in18b \ + cleanup_milter.ref18b1 ../postcat/postcat cleanup_milter.ref18b2 + cp test-queue-file18 test-queue-file18b.tmp + chmod u+w test-queue-file18b.tmp + $(SHLIB_ENV) $(VALGRIND) ./cleanup_milter <cleanup_milter.in18b 2>cleanup_milter.tmp1 + diff cleanup_milter.ref18b1 cleanup_milter.tmp1 + $(SHLIB_ENV) $(VALGRIND) ../postcat/postcat -ov test-queue-file18b.tmp 2>/dev/null >cleanup_milter.tmp2 + diff cleanup_milter.ref18b2 cleanup_milter.tmp2 + rm -f test-queue-file18b.tmp cleanup_milter.tmp1 cleanup_milter.tmp2 + +cleanup_milter_test18c: cleanup_milter test-queue-file18 cleanup_milter.in18c \ + cleanup_milter.ref18c1 ../postcat/postcat cleanup_milter.ref18c2 + cp test-queue-file18 test-queue-file18c.tmp + chmod u+w test-queue-file18c.tmp + $(SHLIB_ENV) $(VALGRIND) ./cleanup_milter <cleanup_milter.in18c 2>cleanup_milter.tmp1 + diff cleanup_milter.ref18c1 cleanup_milter.tmp1 + $(SHLIB_ENV) $(VALGRIND) ../postcat/postcat -ov test-queue-file18c.tmp 2>/dev/null >cleanup_milter.tmp2 + diff cleanup_milter.ref18c2 cleanup_milter.tmp2 + rm -f test-queue-file18c.tmp cleanup_milter.tmp1 cleanup_milter.tmp2 + +cleanup_milter_test18d: cleanup_milter test-queue-file18 cleanup_milter.in18d \ + cleanup_milter.ref18d1 ../postcat/postcat cleanup_milter.ref18d2 + cp test-queue-file18 test-queue-file18d.tmp + chmod u+w test-queue-file18d.tmp + $(SHLIB_ENV) $(VALGRIND) ./cleanup_milter <cleanup_milter.in18d 2>cleanup_milter.tmp1 + diff cleanup_milter.ref18d1 cleanup_milter.tmp1 + $(SHLIB_ENV) $(VALGRIND) ../postcat/postcat -ov test-queue-file18d.tmp 2>/dev/null >cleanup_milter.tmp2 + diff cleanup_milter.ref18d2 cleanup_milter.tmp2 + rm -f test-queue-file18d.tmp cleanup_milter.tmp1 cleanup_milter.tmp2 + depend: $(MAKES) (sed '1,/^# do not edit/!d' Makefile.in; \ set -e; for i in [a-z][a-z0-9]*.c; do \ diff --git a/src/cleanup/cleanup.c b/src/cleanup/cleanup.c index 6fe61f8..5db42bd 100644 --- a/src/cleanup/cleanup.c +++ b/src/cleanup/cleanup.c @@ -262,10 +262,10 @@ /* Available in Postfix version 2.1 and later: /* .IP "\fBsender_bcc_maps (empty)\fR" /* Optional BCC (blind carbon-copy) address lookup tables, indexed -/* by sender address. +/* by envelope sender address. /* .IP "\fBrecipient_bcc_maps (empty)\fR" /* Optional BCC (blind carbon-copy) address lookup tables, indexed by -/* recipient address. +/* envelope recipient address. /* ADDRESS TRANSFORMATION CONTROLS /* .ad /* .fi @@ -303,8 +303,9 @@ /* .PP /* Available in Postfix version 2.0 and later: /* .IP "\fBvirtual_alias_maps ($virtual_maps)\fR" -/* Optional lookup tables that alias specific mail addresses or domains -/* to other local or remote address. +/* Optional lookup tables with aliases that apply to all recipients: +/* \fBlocal\fR(8), virtual, and remote; this is unlike alias_maps that apply +/* only to \fBlocal\fR(8) recipients. /* .PP /* Available in Postfix version 2.2 and later: /* .IP "\fBcanonical_classes (envelope_sender, envelope_recipient, header_sender, header_recipient)\fR" @@ -316,9 +317,10 @@ /* What addresses are subject to sender_canonical_maps address /* mapping. /* .IP "\fBremote_header_rewrite_domain (empty)\fR" -/* Don't rewrite message headers from remote clients at all when -/* this parameter is empty; otherwise, rewrite message headers and -/* append the specified domain name to incomplete addresses. +/* Rewrite or add message headers in mail from remote clients if +/* the remote_header_rewrite_domain parameter value is non-empty, +/* updating incomplete addresses with the domain specified in the +/* remote_header_rewrite_domain parameter, and adding missing headers. /* RESOURCE AND RATE CONTROLS /* .ad /* .fi @@ -365,7 +367,7 @@ /* Preliminary SMTPUTF8 support is introduced with Postfix 3.0. /* .IP "\fBsmtputf8_enable (yes)\fR" /* Enable preliminary SMTPUTF8 support for the protocols described -/* in RFC 6531..6533. +/* in RFC 6531, RFC 6532, and RFC 6533. /* .IP "\fBsmtputf8_autodetect_classes (sendmail, verify)\fR" /* Detect that a message requires SMTPUTF8 support for the specified /* mail origin classes. @@ -433,6 +435,12 @@ /* .IP "\fBinfo_log_address_format (external)\fR" /* The email address form that will be used in non-debug logging /* (info, warning, etc.). +/* .PP +/* Available in Postfix 3.9 and later: +/* .IP "\fBforce_mime_input_conversion (no)\fR" +/* Convert body content that claims to be 8-bit into quoted-printable, +/* before header_checks, body_checks, Milters, and before after-queue +/* content filters. /* FILES /* /etc/postfix/canonical*, canonical mapping table /* /etc/postfix/virtual*, virtual mapping table diff --git a/src/cleanup/cleanup_addr.c b/src/cleanup/cleanup_addr.c index fd8a511..f5e7d91 100644 --- a/src/cleanup/cleanup_addr.c +++ b/src/cleanup/cleanup_addr.c @@ -148,7 +148,7 @@ off_t cleanup_addr_sender(CLEANUP_STATE *state, const char *buf) } /* Fix 20140711: Auto-detect an UTF8 sender. */ if (var_smtputf8_enable && *STR(clean_addr) && !allascii(STR(clean_addr)) - && valid_utf8_string(STR(clean_addr), LEN(clean_addr))) { + && valid_utf8_stringz(STR(clean_addr))) { state->smtputf8 |= SMTPUTF8_FLAG_SENDER; /* Fix 20140713: request SMTPUTF8 support selectively. */ if (state->flags & CLEANUP_FLAG_AUTOUTF8) @@ -216,7 +216,7 @@ void cleanup_addr_recipient(CLEANUP_STATE *state, const char *buf) } /* Fix 20140711: Auto-detect an UTF8 recipient. */ if (var_smtputf8_enable && *STR(clean_addr) && !allascii(STR(clean_addr)) - && valid_utf8_string(STR(clean_addr), LEN(clean_addr))) { + && valid_utf8_stringz(STR(clean_addr))) { /* Fix 20140713: request SMTPUTF8 support selectively. */ if (state->flags & CLEANUP_FLAG_AUTOUTF8) state->smtputf8 |= SMTPUTF8_FLAG_REQUESTED; @@ -275,7 +275,7 @@ void cleanup_addr_bcc_dsn(CLEANUP_STATE *state, const char *bcc, } /* Fix 20140711: Auto-detect an UTF8 recipient. */ if (var_smtputf8_enable && *STR(clean_addr) && !allascii(STR(clean_addr)) - && valid_utf8_string(STR(clean_addr), LEN(clean_addr))) { + && valid_utf8_stringz(STR(clean_addr))) { /* Fix 20140713: request SMTPUTF8 support selectively. */ if (state->flags & CLEANUP_FLAG_AUTOUTF8) state->smtputf8 |= SMTPUTF8_FLAG_REQUESTED; diff --git a/src/cleanup/cleanup_init.c b/src/cleanup/cleanup_init.c index 369a019..446ddf2 100644 --- a/src/cleanup/cleanup_init.c +++ b/src/cleanup/cleanup_init.c @@ -174,6 +174,7 @@ int var_auto_8bit_enc_hdr; /* auto-detect 8bit encoding header */ int var_always_add_hdrs; /* always add missing headers */ int var_virt_addrlen_limit; /* stop exponential growth */ char *var_hfrom_format; /* header_from_format */ +int var_force_mime_iconv; /* force mime downgrade on input */ int var_cleanup_mask_stray_cr_lf; /* replace stray CR or LF with space */ const CONFIG_INT_TABLE cleanup_int_table[] = { @@ -191,6 +192,7 @@ const CONFIG_BOOL_TABLE cleanup_bool_table[] = { VAR_VERP_BOUNCE_OFF, DEF_VERP_BOUNCE_OFF, &var_verp_bounce_off, VAR_AUTO_8BIT_ENC_HDR, DEF_AUTO_8BIT_ENC_HDR, &var_auto_8bit_enc_hdr, VAR_ALWAYS_ADD_HDRS, DEF_ALWAYS_ADD_HDRS, &var_always_add_hdrs, + VAR_FORCE_MIME_ICONV, DEF_FORCE_MIME_ICONV, &var_force_mime_iconv, VAR_CLEANUP_MASK_STRAY_CR_LF, DEF_CLEANUP_MASK_STRAY_CR_LF, &var_cleanup_mask_stray_cr_lf, 0, }; diff --git a/src/cleanup/cleanup_message.c b/src/cleanup/cleanup_message.c index 1ee0a52..0d31598 100644 --- a/src/cleanup/cleanup_message.c +++ b/src/cleanup/cleanup_message.c @@ -1069,6 +1069,9 @@ void cleanup_message(CLEANUP_STATE *state, int type, const char *buf, ssize_t */ mime_options = 0; if (var_disable_mime_input) { + if (var_force_mime_iconv) + msg_fatal("do not specify both %s=yes and %s=yes", + VAR_DISABLE_MIME_INPUT, VAR_FORCE_MIME_ICONV); mime_options |= MIME_OPT_DISABLE_MIME; } else { /* Turn off content checks if bouncing or forwarding mail. */ @@ -1085,6 +1088,8 @@ void cleanup_message(CLEANUP_STATE *state, int type, const char *buf, ssize_t || *var_nesthdr_checks) mime_options |= MIME_OPT_REPORT_NESTING; } + if (var_force_mime_iconv) + mime_options |= MIME_OPT_DOWNGRADE; } state->mime_state = mime_state_alloc(mime_options, cleanup_header_callback, diff --git a/src/cleanup/cleanup_milter.c b/src/cleanup/cleanup_milter.c index 491de25..a35344c 100644 --- a/src/cleanup/cleanup_milter.c +++ b/src/cleanup/cleanup_milter.c @@ -2444,6 +2444,7 @@ static void open_queue_file(CLEANUP_STATE *state, const char *path) long data_offset; long rcpt_count; long qmgr_opts; + const HEADER_OPTS *opts; if (state->dst != 0) { msg_warn("closing %s", cleanup_path); @@ -2455,6 +2456,7 @@ static void open_queue_file(CLEANUP_STATE *state, const char *path) if ((state->dst = vstream_fopen(path, O_RDWR, 0)) == 0) { msg_warn("open %s: %m", path); } else { + var_drop_hdrs = ""; cleanup_path = mystrdup(path); for (;;) { if ((curr_offset = vstream_ftell(state->dst)) < 0) @@ -2511,9 +2513,16 @@ static void open_queue_file(CLEANUP_STATE *state, const char *path) msg_fatal("file %s: vstream_ftell: %m", cleanup_path); } } + } else if (rec_type == REC_TYPE_NORM && state->hop_count == 0 + && (opts = header_opts_find(STR(buf))) != 0 + && opts->type == HDR_RECEIVED) { + state->hop_count += 1; + /* XXX Only the first line of the first Received: header. */ + argv_add(state->auto_hdrs, STR(buf), ARGV_END); } if (state->append_rcpt_pt_offset > 0 && state->append_hdr_pt_offset > 0 + && state->hop_count > 0 && (rec_type == REC_TYPE_END || state->append_meta_pt_offset > 0)) break; diff --git a/src/cleanup/cleanup_milter.in18a b/src/cleanup/cleanup_milter.in18a new file mode 100644 index 0000000..4e60fe4 --- /dev/null +++ b/src/cleanup/cleanup_milter.in18a @@ -0,0 +1,8 @@ +#verbose on +open test-queue-file18a.tmp +# +# Update a prepended header. +# +upd_header 1 Header-Label new-header-value + +close diff --git a/src/cleanup/cleanup_milter.in18b b/src/cleanup/cleanup_milter.in18b new file mode 100644 index 0000000..5f8c12d --- /dev/null +++ b/src/cleanup/cleanup_milter.in18b @@ -0,0 +1,8 @@ +#verbose on +open test-queue-file18b.tmp +# +# Delete a prepended header. +# +del_header 1 Header-Label + +close diff --git a/src/cleanup/cleanup_milter.in18c b/src/cleanup/cleanup_milter.in18c new file mode 100644 index 0000000..bb78c94 --- /dev/null +++ b/src/cleanup/cleanup_milter.in18c @@ -0,0 +1,9 @@ +#verbose on +open test-queue-file18c.tmp +# +# Update the first Received: header. This adds a new header, because +# there is no header that was exposed to the Milter. +# +upd_header 1 Received whatever + +close diff --git a/src/cleanup/cleanup_milter.in18d b/src/cleanup/cleanup_milter.in18d new file mode 100644 index 0000000..607014d --- /dev/null +++ b/src/cleanup/cleanup_milter.in18d @@ -0,0 +1,8 @@ +#verbose on +open test-queue-file18d.tmp +# +# Delete our Received: header. This should do nothing. +# +del_header 1 Received + +close diff --git a/src/cleanup/cleanup_milter.ref18a1 b/src/cleanup/cleanup_milter.ref18a1 new file mode 100644 index 0000000..eab5a83 --- /dev/null +++ b/src/cleanup/cleanup_milter.ref18a1 @@ -0,0 +1 @@ +./cleanup_milter: flags = enable_header_body_filter enable_milters diff --git a/src/cleanup/cleanup_milter.ref18a2 b/src/cleanup/cleanup_milter.ref18a2 new file mode 100644 index 0000000..b5c0477 --- /dev/null +++ b/src/cleanup/cleanup_milter.ref18a2 @@ -0,0 +1,29 @@ +*** ENVELOPE RECORDS test-queue-file18a.tmp *** + 0 message_size: 342 290 1 0 342 0 + 97 message_arrival_time: Tue Dec 12 14:29:04 2023 + 116 create_time: Tue Dec 12 14:29:04 2023 + 140 named_attribute: rewrite_context=local + 163 sender_fullname: Wietse Venema + 178 sender: user@example.com + 196 named_attribute: dsn_orig_rcpt=rfc822;user@example.com + 235 original_recipient: user@example.com + 253 recipient: user@example.com + 271 pointer_record: 0 + 288 *** MESSAGE CONTENTS test-queue-file18a.tmp *** + 290 pointer_record: 653 + 653 regular_text: Header-Label: new-header-value + 685 pointer_record: 318 + 318 regular_text: Received: by wzv.porcupine.org (Postfix, from userid 1000) + 378 regular_text: id 4SqTFD6TVpz4w4n; Tue, 12 Dec 2023 14:29:04 -0500 (EST) + 438 regular_text: Subject: test + 453 padding: 0 + 456 regular_text: Message-Id: <4SqTFD6TVpz4w4n@wzv.porcupine.org> + 505 regular_text: Date: Tue, 12 Dec 2023 14:29:04 -0500 (EST) + 550 regular_text: From: Wietse Venema <user@example.com> + 590 pointer_record: 0 + 607 regular_text: + 609 regular_text: test + 615 pointer_record: 0 + 632 *** HEADER EXTRACTED test-queue-file18a.tmp *** + 634 pointer_record: 0 + 651 *** MESSAGE FILE END test-queue-file18a.tmp *** diff --git a/src/cleanup/cleanup_milter.ref18b1 b/src/cleanup/cleanup_milter.ref18b1 new file mode 100644 index 0000000..eab5a83 --- /dev/null +++ b/src/cleanup/cleanup_milter.ref18b1 @@ -0,0 +1 @@ +./cleanup_milter: flags = enable_header_body_filter enable_milters diff --git a/src/cleanup/cleanup_milter.ref18b2 b/src/cleanup/cleanup_milter.ref18b2 new file mode 100644 index 0000000..d3b4ed3 --- /dev/null +++ b/src/cleanup/cleanup_milter.ref18b2 @@ -0,0 +1,27 @@ +*** ENVELOPE RECORDS test-queue-file18b.tmp *** + 0 message_size: 342 290 1 0 342 0 + 97 message_arrival_time: Tue Dec 12 14:29:04 2023 + 116 create_time: Tue Dec 12 14:29:04 2023 + 140 named_attribute: rewrite_context=local + 163 sender_fullname: Wietse Venema + 178 sender: user@example.com + 196 named_attribute: dsn_orig_rcpt=rfc822;user@example.com + 235 original_recipient: user@example.com + 253 recipient: user@example.com + 271 pointer_record: 0 + 288 *** MESSAGE CONTENTS test-queue-file18b.tmp *** + 290 pointer_record: 318 + 318 regular_text: Received: by wzv.porcupine.org (Postfix, from userid 1000) + 378 regular_text: id 4SqTFD6TVpz4w4n; Tue, 12 Dec 2023 14:29:04 -0500 (EST) + 438 regular_text: Subject: test + 453 padding: 0 + 456 regular_text: Message-Id: <4SqTFD6TVpz4w4n@wzv.porcupine.org> + 505 regular_text: Date: Tue, 12 Dec 2023 14:29:04 -0500 (EST) + 550 regular_text: From: Wietse Venema <user@example.com> + 590 pointer_record: 0 + 607 regular_text: + 609 regular_text: test + 615 pointer_record: 0 + 632 *** HEADER EXTRACTED test-queue-file18b.tmp *** + 634 pointer_record: 0 + 651 *** MESSAGE FILE END test-queue-file18b.tmp *** diff --git a/src/cleanup/cleanup_milter.ref18c1 b/src/cleanup/cleanup_milter.ref18c1 new file mode 100644 index 0000000..eab5a83 --- /dev/null +++ b/src/cleanup/cleanup_milter.ref18c1 @@ -0,0 +1 @@ +./cleanup_milter: flags = enable_header_body_filter enable_milters diff --git a/src/cleanup/cleanup_milter.ref18c2 b/src/cleanup/cleanup_milter.ref18c2 new file mode 100644 index 0000000..84a3449 --- /dev/null +++ b/src/cleanup/cleanup_milter.ref18c2 @@ -0,0 +1,29 @@ +*** ENVELOPE RECORDS test-queue-file18c.tmp *** + 0 message_size: 342 290 1 0 342 0 + 97 message_arrival_time: Tue Dec 12 14:29:04 2023 + 116 create_time: Tue Dec 12 14:29:04 2023 + 140 named_attribute: rewrite_context=local + 163 sender_fullname: Wietse Venema + 178 sender: user@example.com + 196 named_attribute: dsn_orig_rcpt=rfc822;user@example.com + 235 original_recipient: user@example.com + 253 recipient: user@example.com + 271 pointer_record: 0 + 288 *** MESSAGE CONTENTS test-queue-file18c.tmp *** + 290 regular_text: Header-Label: header-value + 318 regular_text: Received: by wzv.porcupine.org (Postfix, from userid 1000) + 378 regular_text: id 4SqTFD6TVpz4w4n; Tue, 12 Dec 2023 14:29:04 -0500 (EST) + 438 regular_text: Subject: test + 453 padding: 0 + 456 regular_text: Message-Id: <4SqTFD6TVpz4w4n@wzv.porcupine.org> + 505 regular_text: Date: Tue, 12 Dec 2023 14:29:04 -0500 (EST) + 550 regular_text: From: Wietse Venema <user@example.com> + 590 pointer_record: 653 + 653 regular_text: Received: whatever + 673 pointer_record: 607 + 607 regular_text: + 609 regular_text: test + 615 pointer_record: 0 + 632 *** HEADER EXTRACTED test-queue-file18c.tmp *** + 634 pointer_record: 0 + 651 *** MESSAGE FILE END test-queue-file18c.tmp *** diff --git a/src/cleanup/cleanup_milter.ref18d1 b/src/cleanup/cleanup_milter.ref18d1 new file mode 100644 index 0000000..eab5a83 --- /dev/null +++ b/src/cleanup/cleanup_milter.ref18d1 @@ -0,0 +1 @@ +./cleanup_milter: flags = enable_header_body_filter enable_milters diff --git a/src/cleanup/cleanup_milter.ref18d2 b/src/cleanup/cleanup_milter.ref18d2 new file mode 100644 index 0000000..b436ba2 --- /dev/null +++ b/src/cleanup/cleanup_milter.ref18d2 @@ -0,0 +1,27 @@ +*** ENVELOPE RECORDS test-queue-file18d.tmp *** + 0 message_size: 342 290 1 0 342 0 + 97 message_arrival_time: Tue Dec 12 14:29:04 2023 + 116 create_time: Tue Dec 12 14:29:04 2023 + 140 named_attribute: rewrite_context=local + 163 sender_fullname: Wietse Venema + 178 sender: user@example.com + 196 named_attribute: dsn_orig_rcpt=rfc822;user@example.com + 235 original_recipient: user@example.com + 253 recipient: user@example.com + 271 pointer_record: 0 + 288 *** MESSAGE CONTENTS test-queue-file18d.tmp *** + 290 regular_text: Header-Label: header-value + 318 regular_text: Received: by wzv.porcupine.org (Postfix, from userid 1000) + 378 regular_text: id 4SqTFD6TVpz4w4n; Tue, 12 Dec 2023 14:29:04 -0500 (EST) + 438 regular_text: Subject: test + 453 padding: 0 + 456 regular_text: Message-Id: <4SqTFD6TVpz4w4n@wzv.porcupine.org> + 505 regular_text: Date: Tue, 12 Dec 2023 14:29:04 -0500 (EST) + 550 regular_text: From: Wietse Venema <user@example.com> + 590 pointer_record: 0 + 607 regular_text: + 609 regular_text: test + 615 pointer_record: 0 + 632 *** HEADER EXTRACTED test-queue-file18d.tmp *** + 634 pointer_record: 0 + 651 *** MESSAGE FILE END test-queue-file18d.tmp *** diff --git a/src/cleanup/test-queue-file18 b/src/cleanup/test-queue-file18 Binary files differnew file mode 100644 index 0000000..42c46f1 --- /dev/null +++ b/src/cleanup/test-queue-file18 diff --git a/src/dns/Makefile.in b/src/dns/Makefile.in index 3ebf75f..5ea09cf 100644 --- a/src/dns/Makefile.in +++ b/src/dns/Makefile.in @@ -11,7 +11,8 @@ DEFS = -I. -I$(INC_DIR) -D$(SYSTYPE) CFLAGS = $(DEBUG) $(OPT) $(DEFS) INCL = LIB = lib$(LIB_PREFIX)dns$(LIB_SUFFIX) -TESTPROG= test_dns_lookup dns_rr_to_pa dns_rr_to_sa dns_sa_to_rr dns_rr_eq_sa +TESTPROG= test_dns_lookup dns_rr_to_pa dns_rr_to_sa dns_sa_to_rr dns_rr_eq_sa \ + dns_rr_test LIBS = ../../lib/lib$(LIB_PREFIX)global$(LIB_SUFFIX) \ ../../lib/lib$(LIB_PREFIX)util$(LIB_SUFFIX) LIB_DIR = ../../lib @@ -31,7 +32,7 @@ test: $(TESTPROG) tests: test dns_rr_to_pa_test dns_rr_to_sa_test dns_sa_to_rr_test \ dns_rr_eq_sa_test no-a-test no-aaaa-test no-mx-test \ error-filter-test nullmx_test nxdomain_test mxonly_test \ - dnsbl_tests + dnsbl_tests dns_rr_tests dnsbl_tests: \ dnsbl_ttl_127.0.0.2_bind_plain_test \ @@ -57,7 +58,7 @@ DNSBL_EXIST_REPLY_FIX = \ -e 's/ [0-9]* [0-9]* [0-9]* [0-9]* [0-9]*/ D D D D D/' \ -e 's/127.0.0.[0-9]*$$/127.0.0.D/' \ | uniq - + root_tests: $(LIB): $(OBJS) @@ -240,6 +241,12 @@ dnsbl_ttl_127.0.0.2_priv_ncache_test: test_dns_lookup dnsbl_ttl_127.0.0.2_bind_p diff dnsbl_ttl_127.0.0.2_bind_plain.ref dnsbl_ttl_127.0.0.2_priv_ncache.tmp rm -f dnsbl_ttl_127.0.0.2_priv_ncache.tmp +dns_rr_tests: dns_rr_test + $(SHLIB_ENV) $(VALGRIND) ./dns_rr_test + +dns_rr_test: dns_rr_test.o $(LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.c $(LIB) $(LIBS) $(SYSLIBS) + printfck: $(OBJS) $(PROG) rm -rf printfck mkdir printfck @@ -319,6 +326,19 @@ dns_rr_filter.o: ../../include/vstream.h dns_rr_filter.o: ../../include/vstring.h dns_rr_filter.o: dns.h dns_rr_filter.o: dns_rr_filter.c +dns_rr_test.o: ../../include/check_arg.h +dns_rr_test.o: ../../include/msg.h +dns_rr_test.o: ../../include/msg_vstream.h +dns_rr_test.o: ../../include/myaddrinfo.h +dns_rr_test.o: ../../include/mymalloc.h +dns_rr_test.o: ../../include/sock_addr.h +dns_rr_test.o: ../../include/stringops.h +dns_rr_test.o: ../../include/sys_defs.h +dns_rr_test.o: ../../include/vbuf.h +dns_rr_test.o: ../../include/vstream.h +dns_rr_test.o: ../../include/vstring.h +dns_rr_test.o: dns.h +dns_rr_test.o: dns_rr_test.c dns_rr_to_pa.o: ../../include/check_arg.h dns_rr_to_pa.o: ../../include/msg.h dns_rr_to_pa.o: ../../include/myaddrinfo.h diff --git a/src/dns/dns_lookup.c b/src/dns/dns_lookup.c index c21b619..4cf9a5f 100644 --- a/src/dns/dns_lookup.c +++ b/src/dns/dns_lookup.c @@ -85,6 +85,12 @@ /* an invalid name is reported as a DNS_INVAL result, while /* malformed replies are reported as transient errors. /* +/* Note: in dns_lookup*() results and queries, a name may start +/* with a "*" label, which is valid according to RFC 1034 +/* section 4.3.3. Such a name will not pass valid_hostname() +/* checks in the rest of Postfix, because it is not a valid +/* host or domain name. +/* /* dns_get_h_errno() returns the last error. This deprecates /* usage of the global h_errno variable. We should not rely /* on that being updated. @@ -300,7 +306,7 @@ typedef struct DNS_REPLY { /* * Use the threadsafe resolver API if available, not because it is - * theadsafe, but because it has more functionality. + * threadsafe, but because it has more functionality. */ #ifdef USE_RES_NCALLS static struct __res_state dns_res_state; diff --git a/src/dns/dns_rr_test.c b/src/dns/dns_rr_test.c new file mode 100644 index 0000000..7bbe769 --- /dev/null +++ b/src/dns/dns_rr_test.c @@ -0,0 +1,433 @@ + /* + * System library. + */ +#include <sys_defs.h> +#include <stdlib.h> + + /* + * Utility library. + */ +#include <msg.h> +#include <msg_vstream.h> +#include <mymalloc.h> +#include <stringops.h> +#include <vstring.h> + + /* + * DNS library. + */ +#include <dns.h> + +#define STR(x) vstring_str(x) + + /* + * Test helpers. TODO: move eq_dns_rr() to testing/dns_rr_testers.c; need to + * verify that the expected difference is reported, or use a GTEST matcher. + */ + +/* print_dns_rr - format as { qname, reply, flags } */ + +static char *print_dns_rr(VSTRING *buf, DNS_RR *rr) +{ + static VSTRING *tmp; + + if (tmp == 0) + tmp = vstring_alloc(100); + vstring_sprintf(buf, "{qname=%s, reply='%s', flags=0x%x}", + rr->qname, dns_strrecord(tmp, rr), rr->flags); + return (STR(buf)); +} + +/* eq_dns_rr - predicate that two lists are equivalent */ + +static int eq_dns_rr(DNS_RR *got, DNS_RR *want) +{ + VSTRING *got_buf = 0; + VSTRING *want_buf = 0; + +#define EQ_DNS_RR_RETURN(val) do { \ + if (got_buf) \ + vstring_free(got_buf); \ + if (want_buf) \ + vstring_free(want_buf); \ + return (val); \ + } while (0) + + /* Same length. */ + if (got == 0 && want == 0) + EQ_DNS_RR_RETURN(1); + if (want == 0) { + msg_warn("got %s, want null", + print_dns_rr(got_buf = vstring_alloc(100), got)); + } + if (got == 0) { + msg_warn("got null, want %s", + print_dns_rr(want_buf = vstring_alloc(100), want)); + EQ_DNS_RR_RETURN(0); + } + /* Same query name, resource record, flags. */ + if (strcmp(print_dns_rr(got_buf = vstring_alloc(100), got), + print_dns_rr(want_buf = vstring_alloc(100), want)) != 0) { + msg_warn("got %s, want %s", STR(want_buf), STR(got_buf)); + EQ_DNS_RR_RETURN(0); + } + /* Same children. */ + EQ_DNS_RR_RETURN(eq_dns_rr(got->next, want->next)); +} + +static int eq_dns_rr_free(DNS_RR *got, DNS_RR *want) +{ + int res = eq_dns_rr(got, want); + + dns_rr_free(got); + dns_rr_free(want); + return (res); +} + + /* + * Tests and test cases. + */ +typedef struct TEST_CASE { + const char *label; /* identifies test case */ + int (*fn) (void); +} TEST_CASE; + +#define PASS (0) +#define FAIL (1) + + /* + * Begin helper tests. TODO: move these to testing/dns_rr_testers_test.c. + */ + +static int eq_dns_rr_qname_differ(void) +{ + DNS_RR *got = dns_rr_create("qa", "ra", T_SRV, C_IN, 3600, 1, 25, 1, "mxa", 4); + DNS_RR *want = dns_rr_copy(got); + + myfree(want->qname); + want->qname = mystrdup("qb"); + return (!eq_dns_rr_free(got, want)); +} + +static int eq_dns_rr_reply_differ(void) +{ + DNS_RR *got = dns_rr_create("qa", "ra", T_SRV, C_IN, 3600, 1, 25, 1, "mxa", 4); + DNS_RR *want = dns_rr_copy(got); + + want->port += 1; + return (!eq_dns_rr_free(got, want)); +} + + /* + * End helper tests. + */ + + /* + * Begin DNS_RR tests. + */ + +static int eq_dns_rr_flags_differ(void) +{ + DNS_RR *got = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *want = dns_rr_copy(got); + + want->flags |= DNS_RR_FLAG_TRUNCATED; + return (!eq_dns_rr_free(got, want)); +} + +static int append_to_null_from_null(void) +{ + DNS_RR *got = dns_rr_append((DNS_RR *) 0, (DNS_RR *) 0); + DNS_RR *want = 0; + + return (eq_dns_rr_free(got, want)); +} + +static int append_to_elem_from_null(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *got, *want; + + got = dns_rr_append(dns_rr_copy(a), (DNS_RR *) 0); + + want = a; + + return (eq_dns_rr_free(got, want)); +} + +static int appent_to_null_from_elem(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *got, *want; + + got = dns_rr_append((DNS_RR *) 0, dns_rr_copy(a)); + + want = a; + + return (eq_dns_rr_free(got, want)); +} + +static int append_to_elem_from_elem(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *b = dns_rr_create_noport("qb", "rb", T_MX, C_IN, 3600, 1, "mxb", 4); + DNS_RR *got, *want; + + got = dns_rr_append(dns_rr_copy(a), dns_rr_copy(b)); + + (want = a)->next = b; + + return (eq_dns_rr_free(got, want)); +} + +static int append_to_elem_from_list(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *b = dns_rr_create_noport("qb", "rb", T_MX, C_IN, 3600, 1, "mxb", 4); + DNS_RR *c = dns_rr_create_noport("qc", "rc", T_MX, C_IN, 3600, 1, "mxc", 4); + DNS_RR *got, *want; + + got = dns_rr_append(dns_rr_copy(a), + dns_rr_append(dns_rr_copy(b), + dns_rr_copy(c))); + + ((want = a)->next = b)->next = c; + + return (eq_dns_rr_free(got, want)); +} + +static int append_to_list_from_elem(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *b = dns_rr_create_noport("qb", "rb", T_MX, C_IN, 3600, 1, "mxb", 4); + DNS_RR *c = dns_rr_create_noport("qc", "rc", T_MX, C_IN, 3600, 1, "mxc", 4); + DNS_RR *got, *want; + + got = dns_rr_append(dns_rr_append(dns_rr_copy(a), + dns_rr_copy(b)), + dns_rr_copy(c)); + + ((want = a)->next = b)->next = c; + + return (eq_dns_rr_free(got, want)); +} + +static int append_to_list_from_list(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *b = dns_rr_create_noport("qb", "rb", T_MX, C_IN, 3600, 1, "mxb", 4); + DNS_RR *c = dns_rr_create_noport("qc", "rc", T_MX, C_IN, 3600, 1, "mxc", 4); + DNS_RR *d = dns_rr_create_noport("qd", "rd", T_MX, C_IN, 3600, 1, "mxd", 4); + DNS_RR *got, *want; + + got = dns_rr_append(dns_rr_append(dns_rr_copy(a), + dns_rr_copy(b)), + dns_rr_append(dns_rr_copy(c), + dns_rr_copy(d))); + + (((want = a)->next = b)->next = c)->next = d; + + return (eq_dns_rr_free(got, want)); +} + +static int append_propagates_flags(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *b = dns_rr_create_noport("qb", "rb", T_MX, C_IN, 3600, 1, "mxb", 4); + DNS_RR *c = dns_rr_create_noport("qc", "rc", T_MX, C_IN, 3600, 1, "mxc", 4); + DNS_RR *d = dns_rr_create_noport("qd", "rd", T_MX, C_IN, 3600, 1, "mxd", 4); + DNS_RR *left = dns_rr_append(dns_rr_copy(a), dns_rr_copy(b)); + DNS_RR *rite = dns_rr_append(dns_rr_copy(c), dns_rr_copy(d)); + DNS_RR *got, *want, *rr; + + for (rr = rite; rr; rr = rr->next) + rr->flags |= DNS_RR_FLAG_TRUNCATED; + + got = dns_rr_append(left, rite); + + (((want = a)->next = b)->next = c)->next = d; + for (rr = want; rr; rr = rr->next) + rr->flags |= DNS_RR_FLAG_TRUNCATED; + + return (eq_dns_rr_free(got, want)); +} + +static int append_to_list_from_list_truncate(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *b = dns_rr_create_noport("qb", "rb", T_MX, C_IN, 3600, 1, "mxb", 4); + DNS_RR *c = dns_rr_create_noport("qc", "rc", T_MX, C_IN, 3600, 1, "mxc", 4); + DNS_RR *d = dns_rr_create_noport("qd", "rd", T_MX, C_IN, 3600, 1, "mxd", 4); + DNS_RR *got, *want, *rr; + + var_dns_rr_list_limit = 3; + + ((want = dns_rr_copy(a))->next = dns_rr_copy(b))->next = dns_rr_copy(c); + for (rr = want; rr; rr = rr->next) + rr->flags |= DNS_RR_FLAG_TRUNCATED; + + got = dns_rr_append(dns_rr_append(a, b), + dns_rr_append(c, d)); + + return (eq_dns_rr_free(got, want)); +} + +static int append_to_list_from_elem_elem_truncate(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *b = dns_rr_create_noport("qb", "rb", T_MX, C_IN, 3600, 1, "mxb", 4); + DNS_RR *c = dns_rr_create_noport("qc", "rc", T_MX, C_IN, 3600, 1, "mxc", 4); + DNS_RR *d = dns_rr_create_noport("qd", "rd", T_MX, C_IN, 3600, 1, "mxd", 4); + DNS_RR *got, *want, *rr; + + var_dns_rr_list_limit = 2; + + (want = dns_rr_copy(a))->next = dns_rr_copy(b); + for (rr = want; rr; rr = rr->next) + rr->flags |= DNS_RR_FLAG_TRUNCATED; + + got = dns_rr_append(a, b); + got = dns_rr_append(got, c); /* should be logged */ + got = dns_rr_append(got, d); /* should be silent */ + + return (eq_dns_rr_free(got, want)); +} + +static int append_to_list_from_elem_truncate(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *b = dns_rr_create_noport("qb", "rb", T_MX, C_IN, 3600, 1, "mxb", 4); + DNS_RR *c = dns_rr_create_noport("qc", "rc", T_MX, C_IN, 3600, 1, "mxc", 4); + DNS_RR *got, *want, *rr; + + var_dns_rr_list_limit = 2; + + (want = dns_rr_copy(a))->next = dns_rr_copy(b); + for (rr = want; rr; rr = rr->next) + rr->flags |= DNS_RR_FLAG_TRUNCATED; + + got = dns_rr_append(dns_rr_append(a, b), c); + + return (eq_dns_rr_free(got, want)); +} + +static int append_to_elem_from_list_truncate(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *b = dns_rr_create_noport("qb", "rb", T_MX, C_IN, 3600, 1, "mxb", 4); + DNS_RR *c = dns_rr_create_noport("qc", "rc", T_MX, C_IN, 3600, 1, "mxc", 4); + DNS_RR *got, *want, *rr; + + var_dns_rr_list_limit = 2; + + (want = dns_rr_copy(a))->next = dns_rr_copy(b); + for (rr = want; rr; rr = rr->next) + rr->flags |= DNS_RR_FLAG_TRUNCATED; + + got = dns_rr_append(a, dns_rr_append(b, c)); + + return (eq_dns_rr_free(got, want)); +} + +static int append_to_list_from_elem_exact_fit(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *b = dns_rr_create_noport("qb", "rb", T_MX, C_IN, 3600, 1, "mxb", 4); + DNS_RR *c = dns_rr_create_noport("qc", "rc", T_MX, C_IN, 3600, 1, "mxc", 4); + DNS_RR *got, *want; + + var_dns_rr_list_limit = 3; + + ((want = dns_rr_copy(a))->next = dns_rr_copy(b))->next = dns_rr_copy(c); + + got = dns_rr_append(dns_rr_append(a, b), c); + + return (eq_dns_rr_free(got, want)); +} + +static int append_to_elem_from_list_exact_fit(void) +{ + DNS_RR *a = dns_rr_create_noport("qa", "ra", T_MX, C_IN, 3600, 1, "mxa", 4); + DNS_RR *b = dns_rr_create_noport("qb", "rb", T_MX, C_IN, 3600, 1, "mxb", 4); + DNS_RR *c = dns_rr_create_noport("qc", "rc", T_MX, C_IN, 3600, 1, "mxc", 4); + DNS_RR *got, *want; + + var_dns_rr_list_limit = 3; + + ((want = dns_rr_copy(a))->next = dns_rr_copy(b))->next = dns_rr_copy(c); + + got = dns_rr_append(a, dns_rr_append(b, c)); + + return (eq_dns_rr_free(got, want)); +} + + /* + * The test cases. + */ +static const TEST_CASE test_cases[] = { + + /* + * Test eq_dns_rr; TODO: move to testing/dns_rr_testers_test.c + */ + "eq_dns_rr qname differ", eq_dns_rr_qname_differ, + "eq_dns_rr reply differ", eq_dns_rr_reply_differ, + "eq_dns_rr flags differ", eq_dns_rr_flags_differ, + + /* + * Test dns_rr_append() without truncation. + */ + "append to null from null", append_to_null_from_null, + "append to null from element", appent_to_null_from_elem, + "append to element from null", append_to_elem_from_null, + "append to element from element", append_to_elem_from_elem, + "append to element from list", append_to_elem_from_list, + "append to list from element", append_to_list_from_elem, + "append to list from list", append_to_list_from_list, + + /* + * Test dns_rr_append() flag propagation. + */ + "append propagates flags", append_propagates_flags, + + /* + * Test dns_rr_append() with truncation. + */ + "append to list from list truncate", append_to_list_from_list_truncate, + "append to list from element element truncate", append_to_list_from_elem_elem_truncate, + "append to list from element truncate", append_to_list_from_elem_truncate, + "append to element from list truncate", append_to_elem_from_list_truncate, + "append to list from element exact fit", append_to_list_from_elem_exact_fit, + "append to element from list exact fit", append_to_elem_from_list_exact_fit, + + /* + * TODO: tests dns_rr_sort(), dns_rr_srv_sort(), dns_rr_remove(), + * dns_rr_shuffle(), etc. + */ + 0, +}; + +int main(int argc, char **argv) +{ + const TEST_CASE *tp; + int pass = 0; + int fail = 0; + VSTRING *res_buf = vstring_alloc(100); + int saved_limit = var_dns_rr_list_limit; + + msg_vstream_init(sane_basename((VSTRING *) 0, argv[0]), VSTREAM_ERR); + + for (tp = test_cases; tp->label != 0; tp++) { + msg_info("RUN %s", tp->label); + if (tp->fn() == 0) { + fail++; + msg_info("FAIL %s", tp->label); + } else { + msg_info("PASS %s", tp->label); + pass++; + } + var_dns_rr_list_limit = saved_limit; + } + msg_info("PASS=%d FAIL=%d", pass, fail); + vstring_free(res_buf); + exit(fail != 0); +} diff --git a/src/dns/mxonly_test.ref b/src/dns/mxonly_test.ref index 44f22d6..e1e4bad 100644 --- a/src/dns/mxonly_test.ref +++ b/src/dns/mxonly_test.ref @@ -6,6 +6,6 @@ ./test_dns_lookup: lookup porcupine.org type A flags RES_USE_DNSSEC ./test_dns_lookup: dns_query: porcupine.org (A): Host found but no data record of requested type ad: 0, rr: porcupine.org. 3600 IN MX 10 spike.porcupine.org. -ad: 0, rr: porcupine.org. 3600 IN MX 30 m1.porcupine.org. ad: 0, rr: porcupine.org. 3600 IN MX 30 vz.porcupine.org. +ad: 0, rr: porcupine.org. 3600 IN MX 40 m1.porcupine.org. porcupine.org: fqdn: porcupine.org diff --git a/src/dns/no-mx.ref b/src/dns/no-mx.ref index 5adc7bf..527e0b8 100644 --- a/src/dns/no-mx.ref +++ b/src/dns/no-mx.ref @@ -1,15 +1,15 @@ ./test_dns_lookup: dict_regexp_lookup: no-mx.reg: porcupine.org. 3600 IN MX 10 spike.porcupine.org. -./test_dns_lookup: dict_regexp_lookup: no-mx.reg: porcupine.org. 3600 IN MX 30 m1.porcupine.org. ./test_dns_lookup: dict_regexp_lookup: no-mx.reg: porcupine.org. 3600 IN MX 30 vz.porcupine.org. +./test_dns_lookup: dict_regexp_lookup: no-mx.reg: porcupine.org. 3600 IN MX 40 m1.porcupine.org. ./test_dns_lookup: dns_get_answer: type MX for porcupine.org ./test_dns_lookup: dns_get_answer: type MX for porcupine.org ./test_dns_lookup: dns_get_answer: type MX for porcupine.org ./test_dns_lookup: dns_query: porcupine.org (MX): OK ./test_dns_lookup: ignoring DNS RR: porcupine.org. 3600 IN MX 10 spike.porcupine.org. -./test_dns_lookup: ignoring DNS RR: porcupine.org. 3600 IN MX 30 m1.porcupine.org. ./test_dns_lookup: ignoring DNS RR: porcupine.org. 3600 IN MX 30 vz.porcupine.org. +./test_dns_lookup: ignoring DNS RR: porcupine.org. 3600 IN MX 40 m1.porcupine.org. ./test_dns_lookup: lookup porcupine.org type MX flags RES_USE_DNSSEC ./test_dns_lookup: maps_find: DNS reply filter: regexp:no-mx.reg(0,lock|fold_fix): porcupine.org. 3600 IN MX 10 spike.porcupine.org. = ignore -./test_dns_lookup: maps_find: DNS reply filter: regexp:no-mx.reg(0,lock|fold_fix): porcupine.org. 3600 IN MX 30 m1.porcupine.org. = ignore ./test_dns_lookup: maps_find: DNS reply filter: regexp:no-mx.reg(0,lock|fold_fix): porcupine.org. 3600 IN MX 30 vz.porcupine.org. = ignore +./test_dns_lookup: maps_find: DNS reply filter: regexp:no-mx.reg(0,lock|fold_fix): porcupine.org. 3600 IN MX 40 m1.porcupine.org. = ignore ./test_dns_lookup: warning: Error looking up name=porcupine.org type=MX: DNS reply filter drops all results (rcode=0) diff --git a/src/dns/test_dns_lookup.c b/src/dns/test_dns_lookup.c index f07c3ef..6970124 100644 --- a/src/dns/test_dns_lookup.c +++ b/src/dns/test_dns_lookup.c @@ -80,7 +80,7 @@ int main(int argc, char **argv) var_dnssec_probe = ""; msg_vstream_init(argv[0], VSTREAM_ERR); - while ((ch = GETOPT(argc, argv, "f:npvs")) > 0) { + while ((ch = GETOPT(argc, argv, "f:l:npvs")) > 0) { switch (ch) { case 'v': msg_verbose++; @@ -88,6 +88,9 @@ int main(int argc, char **argv) case 'f': dns_rr_filter_compile("DNS reply filter", optarg); break; + case 'l': + var_dns_rr_list_limit = atoi(optarg); + break; case 'n': lflags |= DNS_REQ_FLAG_NCACHE_TTL; break; diff --git a/src/dnsblog/dnsblog.c b/src/dnsblog/dnsblog.c index bc87c4b..7a4a446 100644 --- a/src/dnsblog/dnsblog.c +++ b/src/dnsblog/dnsblog.c @@ -43,7 +43,8 @@ /* How much time a Postfix daemon process may take to handle a /* request before it is terminated by a built-in watchdog timer. /* .IP "\fBpostscreen_dnsbl_sites (empty)\fR" -/* Optional list of DNS allow/denylist domains, filters and weight +/* Optional list of patterns with DNS allow/denylist domains, filters +/* and weight /* factors. /* .IP "\fBipc_timeout (3600s)\fR" /* The time limit for sending or receiving information over an internal diff --git a/src/global/Makefile.in b/src/global/Makefile.in index 86390ed..c7a1d36 100644 --- a/src/global/Makefile.in +++ b/src/global/Makefile.in @@ -3,7 +3,7 @@ SRCS = abounce.c anvil_clnt.c been_here.c bounce.c bounce_log.c \ canon_addr.c cfg_parser.c cleanup_strerror.c cleanup_strflags.c \ clnt_stream.c conv_time.c db_common.c debug_peer.c debug_process.c \ defer.c deliver_completed.c deliver_flock.c deliver_pass.c \ - deliver_request.c dict_ldap.c dict_mysql.c dict_pgsql.c \ + deliver_request.c dict_ldap.c dict_mongodb.c dict_mysql.c dict_pgsql.c \ dict_proxy.c dict_sqlite.c domain_list.c dot_lockfile.c dot_lockfile_as.c \ dsb_scan.c dsn.c dsn_buf.c dsn_mask.c dsn_print.c dsn_util.c \ ehlo_mask.c ext_prop.c file_id.c flush_clnt.c header_opts.c \ @@ -80,13 +80,13 @@ OBJS = abounce.o anvil_clnt.o been_here.o bounce.o bounce_log.o \ # MAP_OBJ is for maps that may be dynamically loaded with dynamicmaps.cf. # When hard-linking these maps, makedefs sets NON_PLUGIN_MAP_OBJ=$(MAP_OBJ), # otherwise it sets the PLUGIN_* macros. -MAP_OBJ = dict_ldap.o dict_mysql.o dict_pgsql.o dict_sqlite.o +MAP_OBJ = dict_ldap.o dict_mysql.o dict_pgsql.o dict_sqlite.o dict_mongodb.o HDRS = abounce.h anvil_clnt.h been_here.h bounce.h bounce_log.h \ canon_addr.h cfg_parser.h cleanup_user.h clnt_stream.h config.h \ conv_time.h db_common.h debug_peer.h debug_process.h defer.h \ deliver_completed.h deliver_flock.h deliver_pass.h deliver_request.h \ - dict_ldap.h dict_mysql.h dict_pgsql.h dict_proxy.h dict_sqlite.h domain_list.h \ + dict_ldap.h dict_mysql.h dict_pgsql.h dict_mongodb.h dict_proxy.h dict_sqlite.h domain_list.h \ dot_lockfile.h dot_lockfile_as.h dsb_scan.h dsn.h dsn_buf.h \ dsn_mask.h dsn_print.h dsn_util.h ehlo_mask.h ext_prop.h \ file_id.h flush_clnt.h header_opts.h header_token.h input_transp.h \ @@ -136,7 +136,7 @@ LIBS = ../../lib/lib$(LIB_PREFIX)util$(LIB_SUFFIX) LIB_DIR = ../../lib INC_DIR = ../../include PLUGIN_MAP_SO = $(LIB_PREFIX)ldap$(LIB_SUFFIX) $(LIB_PREFIX)mysql$(LIB_SUFFIX) \ - $(LIB_PREFIX)pgsql$(LIB_SUFFIX) $(LIB_PREFIX)sqlite$(LIB_SUFFIX) + $(LIB_PREFIX)pgsql$(LIB_SUFFIX) $(LIB_PREFIX)sqlite$(LIB_SUFFIX) $(LIB_PREFIX)mongodb$(LIB_SUFFIX) MAKES = .c.o:; $(CC) $(SHLIB_CFLAGS) $(CFLAGS) -c $*.c @@ -173,6 +173,9 @@ $(LIB_PREFIX)pgsql$(LIB_SUFFIX): dict_pgsql.o $(LIB_PREFIX)sqlite$(LIB_SUFFIX): dict_sqlite.o $(PLUGIN_LD) $(SHLIB_RPATH) -o $@ dict_sqlite.o $(AUXLIBS_SQLITE) +$(LIB_PREFIX)mongodb$(LIB_SUFFIX): dict_mongodb.o + $(PLUGIN_LD) $(SHLIB_RPATH) -o $@ dict_mongodb.o $(AUXLIBS_MONGODB) + update: $(LIB_DIR)/$(LIB) $(HDRS) $(PLUGIN_MAP_SO_UPDATE) -for i in $(HDRS); \ do \ @@ -1210,6 +1213,24 @@ dict_memcache.o: dict_memcache.c dict_memcache.o: dict_memcache.h dict_memcache.o: memcache_proto.h dict_memcache.o: string_list.h +dict_mongodb.o: ../../include/argv.h +dict_mongodb.o: ../../include/auto_clnt.h +dict_mongodb.o: ../../include/check_arg.h +dict_mongodb.o: ../../include/dict.h +dict_mongodb.o: ../../include/match_list.h +dict_mongodb.o: ../../include/msg.h +dict_mongodb.o: ../../include/myflock.h +dict_mongodb.o: ../../include/mymalloc.h +dict_mongodb.o: ../../include/stringops.h +dict_mongodb.o: ../../include/sys_defs.h +dict_mongodb.o: ../../include/vbuf.h +dict_mongodb.o: ../../include/vstream.h +dict_mongodb.o: ../../include/vstring.h +dict_mongodb.o: cfg_parser.h +dict_mongodb.o: db_common.h +dict_mongodb.o: dict_mongodb.c +dict_mongodb.o: dict_mongodb.h +dict_mongodb.o: string_list.h dict_mysql.o: ../../include/argv.h dict_mysql.o: ../../include/check_arg.h dict_mysql.o: ../../include/dict.h @@ -1861,6 +1882,7 @@ mail_dict.o: ../../include/vstream.h mail_dict.o: ../../include/vstring.h mail_dict.o: dict_ldap.h mail_dict.o: dict_memcache.h +mail_dict.o: dict_mongodb.h mail_dict.o: dict_mysql.h mail_dict.o: dict_pgsql.h mail_dict.o: dict_proxy.h @@ -1911,6 +1933,7 @@ mail_params.o: ../../include/htable.h mail_params.o: ../../include/inet_addr_list.h mail_params.o: ../../include/inet_proto.h mail_params.o: ../../include/iostuff.h +mail_params.o: ../../include/logwriter.h mail_params.o: ../../include/midna_domain.h mail_params.o: ../../include/mkmap.h mail_params.o: ../../include/msg.h diff --git a/src/global/dict_ldap.c b/src/global/dict_ldap.c index a078721..7310a96 100644 --- a/src/global/dict_ldap.c +++ b/src/global/dict_ldap.c @@ -904,7 +904,7 @@ static int attrdesc_subtype(const char *a1, const char *a2) /* url_attrs - attributes we want from LDAP URL */ -static char **url_attrs(DICT_LDAP *dict_ldap, LDAPURLDesc * url) +static char **url_attrs(DICT_LDAP *dict_ldap, LDAPURLDesc *url) { static ARGV *attrs; char **a1; @@ -1234,7 +1234,7 @@ static const char *dict_ldap_lookup(DICT *dict, const char *name) * Don't frustrate future attempts to make Postfix UTF-8 transparent. */ if ((dict->flags & DICT_FLAG_UTF8_ACTIVE) == 0 - && !valid_utf8_string(name, strlen(name))) { + && !valid_utf8_stringz(name)) { if (msg_verbose) msg_info("%s: %s: Skipping lookup of non-UTF-8 key '%s'", myname, dict_ldap->parser->name, name); diff --git a/src/global/dict_mongodb.c b/src/global/dict_mongodb.c new file mode 100644 index 0000000..18144b7 --- /dev/null +++ b/src/global/dict_mongodb.c @@ -0,0 +1,570 @@ +/*++ +/* NAME +/* dict_mongodb 3 +/* SUMMARY +/* dictionary interface to mongodb, compatible with libmongoc-1.0 +/* SYNOPSIS +/* #include <dict_mongodb.h> +/* +/* DICT *dict_mongodb_open(name, open_flags, dict_flags) +/* const char *name; +/* int open_flags; +/* int dict_flags; +/* DESCRIPTION +/* dict_mongodb_open() opens a MongoDB database, providing a +/* dictionary interface for Postfix mappings. The result is a +/* pointer to the installed dictionary. +/* +/* Configuration parameters are described in mongodb_table(5). +/* +/* Arguments: +/* .IP name +/* Either the path to the MongoDB configuration file (if it +/* starts with '/' or '.'), or the prefix which will be used +/* to obtain main.cf configuration parameters for this search. +/* +/* In the first case, configuration parameters are specified +/* in the file as \fIname\fR=\fIvalue\fR pairs. +/* +/* In the second case, the configuration parameters are prefixed +/* with the value of \fIname\fR and an underscore, and they +/* are specified in main.cf. For example, if this value is +/* \fImongodbconf\fR, the parameters would look like +/* \fImongodbconf_uri\fR, \fImongodbconf_collection\fR, and +/* so on. +/* .IP open_flags +/* Must be O_RDONLY +/* .IP dict_flags +/* See dict_open(3). +/* SEE ALSO +/* dict(3) generic dictionary manager +/* HISTORY +/* .ad +/* .fi +/* MongoDB support was added in Postfix 3.9. +/* AUTHOR(S) +/* Hamid Maadani (hamid@dexo.tech) +/* Dextrous Technologies, LLC +/* +/* Edited by: +/* Wietse Venema +/* porcupine.org +/* +/* Based on prior work by: +/* Stephan Ferraro +/* Aionda GmbH +/*--*/ + + /* + * System library. + */ +#include <sys_defs.h> +#ifdef HAS_MONGODB +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <ctype.h> +#include <inttypes.h> /* C99 PRId64 */ + +#include <bson/bson.h> +#include <mongoc/mongoc.h> + + /* + * Utility library. + */ +#include <dict.h> +#include <msg.h> +#include <mymalloc.h> +#include <vstring.h> +#include <stringops.h> +#include <auto_clnt.h> +#include <vstream.h> + + /* + * Global library. + */ +#include <cfg_parser.h> +#include <db_common.h> + + /* + * Application-specific. + */ +#include <dict_mongodb.h> + + /* + * Initial size for dynamically-allocated buffers. + */ +#ifndef BUFFER_SIZE +#define BUFFER_SIZE 1024 +#endif + +#define INIT_VSTR(buf, len) do { \ + if (buf == 0) \ + buf = vstring_alloc(len); \ + VSTRING_RESET(buf); \ + VSTRING_TERMINATE(buf); \ + } while (0) + +/* Structure of one mongodb dictionary handle. */ +typedef struct { + /* Initialized by dict_mongodb_open(). */ + DICT dict; /* Parent class */ + CFG_PARSER *parser; /* Configuration file parser */ + mongoc_client_t *client; /* Mongo C client handle */ + /* Initialized by mongodb_parse_config(). */ + char *uri; /* mongodb+srv:/*localhost:27017 */ + char *dbname; /* Database name */ + char *collection; /* Collection name */ + char *query_filter; /* db_common_expand() query template */ + char *projection; /* Advanced MongoDB projection */ + char *result_attribute; /* The key(s) to return the data for */ + char *result_format; /* db_common_expand() result_template */ + int expansion_limit; /* Result expansion limit */ + void *ctx; /* db_common handle */ +} DICT_MONGODB; + +/* Per-process initialization. */ +static bool init_done = false; + +/* itoa - int64_t to string */ + +static char *itoa(int64_t val) +{ + static char buf[21] = {0}; + int ret; + + /* + * XXX(Wietse) replaced custom code with standard library calls that + * handle zero, and negative values. + */ +#define PRId64_FORMAT "%" PRId64 + + ret = snprintf(buf, sizeof(buf), PRId64_FORMAT, val); + if (ret < 0) + msg_panic("itoa: output error for '%s'", PRId64_FORMAT); + if (ret >= sizeof(buf)) + msg_panic("itoa: output for '%s' exceeds space %ld", + PRId64_FORMAT, sizeof(buf)); + return (buf); +} + +/* mongodb_parse_config - parse mongodb configuration file */ + +static void mongodb_parse_config(DICT_MONGODB *dict_mongodb, + const char *mongodbcf) +{ + CFG_PARSER *p = dict_mongodb->parser; + + /* + * Parse the configuration file. + */ + dict_mongodb->uri = cfg_get_str(p, "uri", NULL, 1, 0); + dict_mongodb->dbname = cfg_get_str(p, "dbname", NULL, 1, 0); + dict_mongodb->collection = cfg_get_str(p, "collection", NULL, 1, 0); + dict_mongodb->query_filter = cfg_get_str(p, "query_filter", NULL, 1, 0); + + /* + * One of projection and result_attribute must be specified. That is + * enforced in the caller. + */ + dict_mongodb->projection = cfg_get_str(p, "projection", NULL, 0, 0); + dict_mongodb->result_attribute + = cfg_get_str(p, "result_attribute", NULL, 0, 0); + dict_mongodb->result_format + = cfg_get_str(dict_mongodb->parser, "result_format", "%s", 1, 0); + dict_mongodb->expansion_limit + = cfg_get_int(dict_mongodb->parser, "expansion_limit", 10, 0, 100); + + /* + * db_common query parsing and domain pattern lookup. + */ + dict_mongodb->ctx = 0; + (void) db_common_parse(&dict_mongodb->dict, &dict_mongodb->ctx, + dict_mongodb->query_filter, 1); + db_common_parse_domain(dict_mongodb->parser, dict_mongodb->ctx); +} + +/* expand_value - expand lookup result value */ + +static bool expand_value(DICT_MONGODB *dict_mongodb, const char *p, + const char *lookup_name, + VSTRING *resultString, + int *expansion, const char *key) +{ + + /* + * If a lookup result cannot be processed due to an expansion limit + * error, return a DICT_ERR_RETRY error code and a 'false' result value. + * As documented for many dict_xxx() implementations, and expansion limit + * error is considered a temporary error. + */ + if (dict_mongodb->expansion_limit > 0 + && ++(*expansion) > dict_mongodb->expansion_limit) { + msg_warn("%s:%s: expansion limit exceeded for key: '%s'", + dict_mongodb->dict.type, dict_mongodb->dict.name, key); + dict_mongodb->dict.error = DICT_ERR_RETRY; + return (false); + } + + /* + * XXX(Wietse) Added the dict_mongodb_lookup() lookup_name argument, + * because it selects code paths inside db_common_expand() that are + * specifically for lookup results instead of lookup keys, including + * %[SUD] substitution. + */ + db_common_expand(dict_mongodb->ctx, dict_mongodb->result_format, p, + lookup_name, resultString, 0); + return (true); +} + +/* get_result_string - convert lookup result to string, or set dict.error */ + +static char *get_result_string(DICT_MONGODB *dict_mongodb, + VSTRING *resultString, + bson_iter_t *iter, + const char *lookup_name, + int *expansion, + const char *key) +{ + char *p = NULL; + bool got_one_result = false; + + /* + * If a lookup result cannot be processed due to an error, return a + * non-zero error code and a NULL result value. + */ + INIT_VSTR(resultString, BUFFER_SIZE); + while (dict_mongodb->dict.error == DICT_ERR_NONE && bson_iter_next(iter)) { + switch (bson_iter_type(iter)) { + case BSON_TYPE_UTF8: + p = (char *) bson_iter_utf8(iter, NULL); + if (!bson_utf8_validate(p, strlen(p), true)) { + msg_warn("%s:%s: invalid UTF-8 in lookup result '%s'", + dict_mongodb->dict.type, dict_mongodb->dict.name, p); + dict_mongodb->dict.error = DICT_ERR_RETRY; + break; + } + got_one_result |= expand_value(dict_mongodb, p, lookup_name, + resultString, expansion, key); + break; + case BSON_TYPE_INT64: + case BSON_TYPE_INT32: + p = itoa(bson_iter_as_int64(iter)); + got_one_result |= expand_value(dict_mongodb, p, lookup_name, + resultString, expansion, key); + break; + case BSON_TYPE_ARRAY: + ; /* For pre-C23 Clang. */ + const uint8_t *dataBuffer = NULL; + unsigned int len = 0; + bson_iter_t dataIter; + bson_t *data = NULL; + + /* + * XXX(Wietse) are there any non-error cases, such as a valid but + * empty array, where bson_new_from_data() or bson_iter_init() + * would return null or false? If there are no such cases then we + * must handle null/false as an error. + */ + bson_iter_array(iter, &len, &dataBuffer); + if ((data = bson_new_from_data(dataBuffer, len)) != 0 + && bson_iter_init(&dataIter, data)) { + VSTRING *iterResult = vstring_alloc(BUFFER_SIZE); + + if ((p = get_result_string(dict_mongodb, iterResult, &dataIter, + lookup_name, expansion, key)) != 0) { + vstring_sprintf_append(resultString, (got_one_result) ? + ",%s" : "%s", p); + got_one_result |= true; + } + vstring_free(iterResult); + } + bson_destroy(data); + break; + default: + /* Unexpected field type. As documented, warn and ignore. */ + msg_warn("%s:%s: failed to retrieve value of '%s', " + "Unknown result type %d.", dict_mongodb->dict.type, + dict_mongodb->dict.name, bson_iter_key(iter), + bson_iter_type(iter)); + break; + } + } + if (dict_mongodb->dict.error != DICT_ERR_NONE || !got_one_result) + return (0); + return (vstring_str(resultString)); +} + +/* dict_mongdb_quote - quote json string */ + +static void dict_mongdb_quote(DICT *dict, const char *name, VSTRING *result) +{ + /* quote_for_json_append() will resize the result buffer as needed. */ + (void) quote_for_json_append(result, name, -1); +} + +/* dict_mongdb_append_result_attributes - projection builder */ + +static int dict_mongdb_append_result_attribute(bson_t * projection, + const char *result_attribute) +{ + char *ra = mystrdup(result_attribute); + char *pp = ra; + char *cp; + int ok = 1; + + while (ok && (cp = mystrtok(&pp, CHARS_COMMA_SP)) != 0) + ok = BSON_APPEND_INT32(projection, cp, 1); + myfree(ra); + return (ok); +} + +/* dict_mongodb_lookup - find database entry using mongo query language */ + +static const char *dict_mongodb_lookup(DICT *dict, const char *name) +{ + DICT_MONGODB *dict_mongodb = (DICT_MONGODB *) dict; + mongoc_collection_t *coll = NULL; + mongoc_cursor_t *cursor = NULL; + bson_iter_t iter; + const bson_t *doc = NULL; + bson_t *query = NULL; + bson_t *options = NULL; + bson_t *projection = NULL; + bson_error_t error; + char *result = NULL; + static VSTRING *queryString = NULL; + static VSTRING *resultString = NULL; + int domain_rc; + int expansion = 0; + + dict_mongodb->dict.error = DICT_ERR_NONE; + + /* + * If they specified a domain list for this map, then only search for + * addresses in domains on the list. This can significantly reduce the + * load on the database. + */ + if ((domain_rc = db_common_check_domain(dict_mongodb->ctx, name)) == 0) { + if (msg_verbose) + msg_info("%s:%s: skipping lookup of '%s': domain mismatch", + dict_mongodb->dict.type, dict_mongodb->dict.name, name); + return (0); + } else if (domain_rc < 0) { + DICT_ERR_VAL_RETURN(dict, domain_rc, (char *) 0); + } + + /* + * Ugly macros to make error and non-error handling code more readable. + * If code size is a concern, them an optimizing compiler can eliminate + * dead code or duplicated code. + */ + + /* Set an error code, and return null. */ +#define DICT_MONGODB_LOOKUP_ERR_RETURN(err) do { \ + dict_mongodb->dict.error = (err); \ + DICT_MONGODB_LOOKUP_RETURN((char *) 0); \ +} while (0); + + /* Pass through any error, and return the specified value. */ +#define DICT_MONGODB_LOOKUP_RETURN(val) do { \ + if (coll) mongoc_collection_destroy(coll); \ + if (cursor) mongoc_cursor_destroy(cursor); \ + if (query) bson_destroy(query); \ + if (options) bson_destroy(options); \ + if (projection) bson_destroy(projection); \ + return (val); \ + } while (0) + + coll = mongoc_client_get_collection(dict_mongodb->client, + dict_mongodb->dbname, + dict_mongodb->collection); + if (!coll) { + msg_warn("%s:%s: failed to get collection [%s] from [%s]", + dict_mongodb->dict.type, dict_mongodb->dict.name, + dict_mongodb->collection, dict_mongodb->dbname); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + + /* + * Use the specified result projection, or craft one from the + * result_attribute. Exclude the _id field from the result. + */ + options = bson_new(); + if (dict_mongodb->projection) { + projection = bson_new_from_json((uint8_t *) dict_mongodb->projection, + -1, &error); + if (!projection) { + msg_warn("%s:%s: failed to create a projection from '%s': %s", + dict_mongodb->dict.type, dict_mongodb->dict.name, + dict_mongodb->projection, error.message); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + if (!BSON_APPEND_INT32(projection, "_id", 0) + || !BSON_APPEND_DOCUMENT(options, "projection", projection)) { + msg_warn("%s:%s: failed to append a projection from '%s'", + dict_mongodb->dict.type, dict_mongodb->dict.name, + dict_mongodb->projection); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + } else if (dict_mongodb->result_attribute) { + bson_t res_attr; + + if (!BSON_APPEND_DOCUMENT_BEGIN(options, "projection", &res_attr) + || !BSON_APPEND_INT32(&res_attr, "_id", 0) + || !dict_mongdb_append_result_attribute(&res_attr, + dict_mongodb->result_attribute) + || !bson_append_document_end(options, &res_attr)) { + msg_warn("%s:%s: failed to append a projection from '%s'", + dict_mongodb->dict.type, dict_mongodb->dict.name, + dict_mongodb->result_attribute); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + } else { + /* Can't happen. The configuration parser should reject this. */ + msg_panic("%s:%s: empty 'projection' and 'result_attribute'", + dict_mongodb->dict.type, dict_mongodb->dict.name); + } + + /* + * Expand filter template. This uses a quoting function to prevent + * metacharacter injection with parts from a crafted email address. + */ + INIT_VSTR(queryString, BUFFER_SIZE); + if (!db_common_expand(dict_mongodb->ctx, dict_mongodb->query_filter, + name, 0, queryString, dict_mongdb_quote)) + /* Suppress the actual lookup if the expansion is empty. */ + DICT_MONGODB_LOOKUP_RETURN(0); + + /* Create the query from the expanded query template. */ + query = bson_new_from_json((uint8_t *) vstring_str(queryString), + -1, &error); + if (!query) { + msg_warn("%s:%s: failed to create a query from '%s': %s", + dict_mongodb->dict.type, dict_mongodb->dict.name, + vstring_str(queryString), error.message); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + /* Run the query. */ + cursor = mongoc_collection_find_with_opts(coll, query, options, NULL); + if (mongoc_cursor_error(cursor, &error)) { + msg_warn("%s:%s: cursor error for '%s': %s", + dict_mongodb->dict.type, dict_mongodb->dict.name, + vstring_str(queryString), error.message); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + /* Convert the lookup result to C string. */ + INIT_VSTR(resultString, BUFFER_SIZE); + while (mongoc_cursor_next(cursor, &doc)) { + if (bson_iter_init(&iter, doc)) { + result = get_result_string(dict_mongodb, resultString, &iter, + name, &expansion, name); + } + } + DICT_MONGODB_LOOKUP_RETURN(result); +} + +/* dict_mongodb_close - close MongoDB database */ + +static void dict_mongodb_close(DICT *dict) +{ + DICT_MONGODB *dict_mongodb = (DICT_MONGODB *) dict; + + cfg_parser_free(dict_mongodb->parser); + if (dict_mongodb->ctx) { + db_common_free_ctx(dict_mongodb->ctx); + } + myfree(dict_mongodb->uri); + myfree(dict_mongodb->dbname); + myfree(dict_mongodb->collection); + myfree(dict_mongodb->query_filter); + + if (dict_mongodb->result_attribute) { + myfree(dict_mongodb->result_attribute); + } + if (dict_mongodb->result_format) { + myfree(dict_mongodb->result_format); + } + if (dict_mongodb->projection) { + myfree(dict_mongodb->projection); + } + if (dict_mongodb->client) { + mongoc_client_destroy(dict_mongodb->client); + } + dict_free(dict); +} + +/* dict_mongodb_open - open MongoDB database connection */ + +DICT *dict_mongodb_open(const char *name, int open_flags, int dict_flags) +{ + DICT_MONGODB *dict_mongodb; + CFG_PARSER *parser; + mongoc_uri_t *uri = 0; + bson_error_t error; + + /* Sanity checks. */ + if (open_flags != O_RDONLY) { + return (dict_surrogate(DICT_TYPE_MONGODB, name, open_flags, dict_flags, + "%s:%s: map requires O_RDONLY access mode", + DICT_TYPE_MONGODB, name)); + } + /* Open the configuration file. */ + if ((parser = cfg_parser_alloc(name)) == 0) { + return (dict_surrogate(DICT_TYPE_MONGODB, name, open_flags, dict_flags, + "open %s: %m", name)); + } + /* Create the dictionary object. */ + dict_mongodb = (DICT_MONGODB *) dict_alloc(DICT_TYPE_MONGODB, name, + sizeof(*dict_mongodb)); + dict_mongodb->dict.lookup = dict_mongodb_lookup; + dict_mongodb->dict.close = dict_mongodb_close; + dict_mongodb->dict.flags = dict_flags; + dict_mongodb->parser = parser; + dict_mongodb->dict.owner = cfg_get_owner(dict_mongodb->parser); + dict_mongodb->client = NULL; + + /* Parse config. */ + mongodb_parse_config(dict_mongodb, name); + if (!dict_mongodb->projection == !dict_mongodb->result_attribute) { + dict_mongodb_close(&dict_mongodb->dict); + return (dict_surrogate(DICT_TYPE_MONGODB, name, open_flags, dict_flags, + "%s:%s: specify exactly one of 'projection' or 'result_attribute'", + DICT_TYPE_MONGODB, name)); + } + /* One-time initialization of libmongoc 's internals. */ + if (!init_done) { + mongoc_init(); + init_done = true; + } +#define DICT_MONGODB_OPEN_ERR_RETURN(d) do { \ + DICT *_d = (d); \ + if (uri) mongoc_uri_destroy(uri); \ + dict_mongodb_close(&dict_mongodb->dict); \ + return (_d); \ + } while (0); + + uri = mongoc_uri_new_with_error(dict_mongodb->uri, &error); + if (!uri) + DICT_MONGODB_OPEN_ERR_RETURN(dict_surrogate(DICT_TYPE_MONGODB, name, + open_flags, dict_flags, + "%s:%s: failed to parse URI '%s': %s", + DICT_TYPE_MONGODB, name, + dict_mongodb->uri, error.message)); + + dict_mongodb->client = mongoc_client_new_from_uri_with_error(uri, &error); + if (!dict_mongodb->client) + DICT_MONGODB_OPEN_ERR_RETURN(dict_surrogate(DICT_TYPE_MONGODB, name, + open_flags, dict_flags, + "%s:%s: failed to create client for '%s': %s", + DICT_TYPE_MONGODB, name, + dict_mongodb->uri, + error.message)); + + mongoc_uri_destroy(uri); + mongoc_client_set_error_api(dict_mongodb->client, MONGOC_ERROR_API_VERSION_2); + return (DICT_DEBUG (&dict_mongodb->dict)); +} + +#endif diff --git a/src/global/dict_mongodb.h b/src/global/dict_mongodb.h new file mode 100755 index 0000000..d5120cb --- /dev/null +++ b/src/global/dict_mongodb.h @@ -0,0 +1,43 @@ +#ifndef _DICT_MONGODB_INCLUDED_ +#define _DICT_MONGODB_INCLUDED_ + +/*++ +/* NAME +/* dict_mongodb 3h +/* SUMMARY +/* dictionary interface to mongodb databases +/* SYNOPSIS +/* #include <dict_mongodb.h> +/* DESCRIPTION +/* .nf + + /* + * Utility library. + */ +#include <dict.h> + + /* + * External interface. + */ +#define DICT_TYPE_MONGODB "mongodb" + +extern DICT *dict_mongodb_open(const char *, int, int); + +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Hamid Maadani (hamid@dexo.tech) +/* Dextrous Technologies, LLC +/* +/* Edited by: +/* Wietse Venema +/* porcupine.org +/* +/* Based on prior work by: +/* Stephan Ferraro +/* Aionda GmbH +/*--*/ + +#endif diff --git a/src/global/dict_mysql.c b/src/global/dict_mysql.c index 3c8fe4f..133cc0d 100644 --- a/src/global/dict_mysql.c +++ b/src/global/dict_mysql.c @@ -83,6 +83,10 @@ #include <limits.h> #include <errno.h> +#if !defined(MYSQL_VERSION_ID) || MYSQL_VERSION_ID < 40000 +#error "MySQL versions <4 are no longer supported" +#endif + #ifdef STRCASECMP_IN_STRINGS_H #include <strings.h> #endif @@ -147,9 +151,11 @@ typedef struct { char *username; char *password; char *dbname; + char *charset; + int retry_interval; + int idle_interval; ARGV *hosts; PLMYSQL *pldb; -#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 HOST *active_host; char *tls_cert_file; char *tls_key_file; @@ -159,7 +165,6 @@ typedef struct { #if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT) int tls_verify_cert; #endif -#endif int require_result_set; } DICT_MYSQL; @@ -171,15 +176,15 @@ typedef struct { #define TYPEINET (1<<1) #define RETRY_CONN_MAX 100 -#define RETRY_CONN_INTV 60 /* 1 minute */ -#define IDLE_CONN_INTV 60 /* 1 minute */ +#define DEF_RETRY_INTV 60 /* 1 minute */ +#define DEF_IDLE_INTV 60 /* 1 minute */ /* internal function declarations */ static PLMYSQL *plmysql_init(ARGV *); static int plmysql_query(DICT_MYSQL *, const char *, VSTRING *, MYSQL_RES **); static void plmysql_dealloc(PLMYSQL *); static void plmysql_close_host(HOST *); -static void plmysql_down_host(HOST *); +static void plmysql_down_host(HOST *, int); static void plmysql_connect_single(DICT_MYSQL *, HOST *); static const char *dict_mysql_lookup(DICT *, const char *); DICT *dict_mysql_open(const char *, int, int); @@ -205,13 +210,21 @@ static void dict_mysql_quote(DICT *dict, const char *name, VSTRING *result) buflen = 2 * len + 1; VSTRING_SPACE(result, buflen); -#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 - if (dict_mysql->active_host) - mysql_real_escape_string(dict_mysql->active_host->db, - vstring_end(result), name, len); - else + if (dict_mysql->active_host == 0) + msg_panic("dict_mysql_quote: no active host"); +#if MYSQL_VERSION_ID >= 50706 && !defined(MARIADB_VERSION_ID) + mysql_real_escape_string_quote(dict_mysql->active_host->db, + vstring_end(result), name, len, '\''); +#else + if (mysql_real_escape_string(dict_mysql->active_host->db, + vstring_end(result), name, len) == + (unsigned long) -1) { + msg_warn("dict_mysql: host (%s) cannot escape input string: >%s<", + dict_mysql->active_host->hostname, + mysql_error(dict_mysql->active_host->db)); + dict_mysql->active_host->stat = STATFAIL; + } #endif - mysql_escape_string(vstring_end(result), name, len); VSTRING_SKIP(result); } @@ -231,7 +244,6 @@ static const char *dict_mysql_lookup(DICT *dict, const char *name) int numrows; int expansion; const char *r; - db_quote_callback_t quote_func = dict_mysql_quote; int domain_rc; dict->error = 0; @@ -241,7 +253,7 @@ static const char *dict_mysql_lookup(DICT *dict, const char *name) */ #ifdef SNAPSHOT if ((dict->flags & DICT_FLAG_UTF8_ACTIVE) == 0 - && !valid_utf8_string(name, strlen(name))) { + && !valid_utf8_stringz(name)) { if (msg_verbose) msg_info("%s: %s: Skipping lookup of non-UTF-8 key '%s'", myname, dict_mysql->parser->name, name); @@ -291,11 +303,8 @@ static const char *dict_mysql_lookup(DICT *dict, const char *name) * quoting happens separately for each connection, we don't bother with * quoting... */ -#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 - quote_func = 0; -#endif if (!db_common_expand(dict_mysql->ctx, dict_mysql->query, - name, 0, query, quote_func)) + name, 0, query, (db_quote_callback_t) 0)) return (0); /* do the query - set dict->error & cleanup if there's an error */ @@ -439,8 +448,12 @@ static int plmysql_query(DICT_MYSQL *dict_mysql, { HOST *host; MYSQL_RES *first_result = 0; + + /* In case all hosts are down. */ int query_error = 1; + errno = ENOTSUP; + /* * Helper to avoid spamming the log with warnings. */ @@ -454,8 +467,6 @@ static int plmysql_query(DICT_MYSQL *dict_mysql, while ((host = dict_mysql_get_active(dict_mysql)) != NULL) { -#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 - /* * The active host is used to escape strings in the context of the * active connection's character encoding. @@ -465,8 +476,14 @@ static int plmysql_query(DICT_MYSQL *dict_mysql, VSTRING_TERMINATE(query); db_common_expand(dict_mysql->ctx, dict_mysql->query, name, 0, query, dict_mysql_quote); + /* Check for potential dict_mysql_quote() failure. */ + if (host->stat == STATFAIL) { + plmysql_down_host(host, dict_mysql->retry_interval); + continue; + } + if (msg_verbose) + msg_info("expanded and quoted query: >%s<", vstring_str(query)); dict_mysql->active_host = 0; -#endif query_error = 0; errno = 0; @@ -546,7 +563,7 @@ static int plmysql_query(DICT_MYSQL *dict_mysql, * See what we got. */ if (query_error) { - plmysql_down_host(host); + plmysql_down_host(host, dict_mysql->retry_interval); if (errno == 0) errno = ENOTSUP; if (first_result) { @@ -559,7 +576,7 @@ static int plmysql_query(DICT_MYSQL *dict_mysql, dict_mysql->dict.type, dict_mysql->dict.name, host->hostname); event_request_timer(dict_mysql_event, (void *) host, - IDLE_CONN_INTV); + dict_mysql->idle_interval); break; } } @@ -581,7 +598,6 @@ static void plmysql_connect_single(DICT_MYSQL *dict_mysql, HOST *host) mysql_options(host->db, MYSQL_READ_DEFAULT_FILE, dict_mysql->option_file); if (dict_mysql->option_group && dict_mysql->option_group[0]) mysql_options(host->db, MYSQL_READ_DEFAULT_GROUP, dict_mysql->option_group); -#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 if (dict_mysql->tls_key_file || dict_mysql->tls_cert_file || dict_mysql->tls_CAfile || dict_mysql->tls_CApath || dict_mysql->tls_ciphers) mysql_ssl_set(host->db, @@ -593,7 +609,6 @@ static void plmysql_connect_single(DICT_MYSQL *dict_mysql, HOST *host) mysql_options(host->db, DICT_MYSQL_SSL_VERIFY_SERVER_CERT, &dict_mysql->tls_verify_cert); #endif -#endif if (mysql_real_connect(host->db, (host->type == TYPEINET ? host->name : 0), dict_mysql->username, @@ -602,6 +617,12 @@ static void plmysql_connect_single(DICT_MYSQL *dict_mysql, HOST *host) host->port, (host->type == TYPEUNIX ? host->name : 0), CLIENT_MULTI_RESULTS)) { + if (mysql_set_character_set(host->db, dict_mysql->charset) != 0) { + msg_warn("dict_mysql: mysql_set_character_set '%s' failed: %s", + dict_mysql->charset, mysql_error(host->db)); + plmysql_down_host(host, dict_mysql->retry_interval); + return; + } if (msg_verbose) msg_info("dict_mysql: successful connection to host %s", host->hostname); @@ -609,7 +630,7 @@ static void plmysql_connect_single(DICT_MYSQL *dict_mysql, HOST *host) } else { msg_warn("connect to mysql server %s: %s", host->hostname, mysql_error(host->db)); - plmysql_down_host(host); + plmysql_down_host(host, dict_mysql->retry_interval); } } @@ -625,11 +646,11 @@ static void plmysql_close_host(HOST *host) * plmysql_down_host - close a failed connection AND set a "stay away from * this host" timer */ -static void plmysql_down_host(HOST *host) +static void plmysql_down_host(HOST *host, int retry_interval) { mysql_close(host->db); host->db = 0; - host->ts = time((time_t *) 0) + RETRY_CONN_INTV; + host->ts = time((time_t *) 0) + retry_interval; host->stat = STATFAIL; event_cancel_timer(dict_mysql_event, (void *) host); } @@ -646,10 +667,14 @@ static void mysql_parse_config(DICT_MYSQL *dict_mysql, const char *mysqlcf) dict_mysql->username = cfg_get_str(p, "user", "", 0, 0); dict_mysql->password = cfg_get_str(p, "password", "", 0, 0); dict_mysql->dbname = cfg_get_str(p, "dbname", "", 1, 0); + dict_mysql->charset = cfg_get_str(p, "charset", "utf8mb4", 1, 0); + dict_mysql->retry_interval = cfg_get_int(p, "retry_interval", + DEF_RETRY_INTV, 1, 0); + dict_mysql->idle_interval = cfg_get_int(p, "idle_interval", + DEF_IDLE_INTV, 1, 0); dict_mysql->result_format = cfg_get_str(p, "result_format", "%s", 1, 0); dict_mysql->option_file = cfg_get_str(p, "option_file", NULL, 0, 0); dict_mysql->option_group = cfg_get_str(p, "option_group", "client", 0, 0); -#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 dict_mysql->tls_key_file = cfg_get_str(p, "tls_key_file", NULL, 0, 0); dict_mysql->tls_cert_file = cfg_get_str(p, "tls_cert_file", NULL, 0, 0); dict_mysql->tls_CAfile = cfg_get_str(p, "tls_CAfile", NULL, 0, 0); @@ -658,7 +683,6 @@ static void mysql_parse_config(DICT_MYSQL *dict_mysql, const char *mysqlcf) #if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT) dict_mysql->tls_verify_cert = cfg_get_bool(p, "tls_verify_cert", -1); #endif -#endif dict_mysql->require_result_set = cfg_get_bool(p, "require_result_set", 1); /* @@ -741,9 +765,7 @@ DICT *dict_mysql_open(const char *name, int open_flags, int dict_flags) dict_mysql->dict.flags = dict_flags; dict_mysql->parser = parser; mysql_parse_config(dict_mysql, name); -#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 dict_mysql->active_host = 0; -#endif dict_mysql->pldb = plmysql_init(dict_mysql->hosts); if (dict_mysql->pldb == NULL) msg_fatal("couldn't initialize pldb!\n"); @@ -826,13 +848,13 @@ static void dict_mysql_close(DICT *dict) myfree(dict_mysql->username); myfree(dict_mysql->password); myfree(dict_mysql->dbname); + myfree(dict_mysql->charset); myfree(dict_mysql->query); myfree(dict_mysql->result_format); if (dict_mysql->option_file) myfree(dict_mysql->option_file); if (dict_mysql->option_group) myfree(dict_mysql->option_group); -#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 if (dict_mysql->tls_key_file) myfree(dict_mysql->tls_key_file); if (dict_mysql->tls_cert_file) @@ -843,7 +865,6 @@ static void dict_mysql_close(DICT *dict) myfree(dict_mysql->tls_CApath); if (dict_mysql->tls_ciphers) myfree(dict_mysql->tls_ciphers); -#endif if (dict_mysql->hosts) argv_free(dict_mysql->hosts); if (dict_mysql->ctx) diff --git a/src/global/dict_pgsql.c b/src/global/dict_pgsql.c index 5992135..c626854 100644 --- a/src/global/dict_pgsql.c +++ b/src/global/dict_pgsql.c @@ -108,18 +108,18 @@ #define TYPEUNIX (1<<0) #define TYPEINET (1<<1) -#define TYPECONNSTRING (1<<2) +#define TYPECONNSTR (1<<2) #define RETRY_CONN_MAX 100 -#define RETRY_CONN_INTV 60 /* 1 minute */ -#define IDLE_CONN_INTV 60 /* 1 minute */ +#define DEF_RETRY_INTV 60 /* 1 minute */ +#define DEF_IDLE_INTV 60 /* 1 minute */ typedef struct { PGconn *db; char *hostname; char *name; char *port; - unsigned type; /* TYPEUNIX | TYPEINET | TYPECONNSTRING */ + unsigned type; /* TYPEUNIX | TYPEINET | TYPECONNSTR */ unsigned stat; /* STATUNTRIED | STATFAIL | STATCUR */ time_t ts; /* used for attempting reconnection */ } HOST; @@ -140,6 +140,8 @@ typedef struct { char *password; char *dbname; char *encoding; + int retry_interval; + int idle_interval; char *table; ARGV *hosts; PLPGSQL *pldb; @@ -152,12 +154,11 @@ typedef struct { /* internal function declarations */ static PLPGSQL *plpgsql_init(ARGV *); -static PGSQL_RES *plpgsql_query(DICT_PGSQL *, const char *, VSTRING *, char *, - char *, char *, char *); +static PGSQL_RES *plpgsql_query(DICT_PGSQL *, const char *, VSTRING *); static void plpgsql_dealloc(PLPGSQL *); static void plpgsql_close_host(HOST *); -static void plpgsql_down_host(HOST *); -static void plpgsql_connect_single(HOST *, char *, char *, char *, char *); +static void plpgsql_down_host(HOST *, int); +static void plpgsql_connect_single(DICT_PGSQL *, HOST *); static const char *dict_pgsql_lookup(DICT *, const char *); DICT *dict_pgsql_open(const char *, int, int); static void dict_pgsql_close(DICT *); @@ -280,7 +281,7 @@ static const char *dict_pgsql_lookup(DICT *dict, const char *name) */ #ifdef SNAPSHOT if ((dict->flags & DICT_FLAG_UTF8_ACTIVE) == 0 - && !valid_utf8_string(name, strlen(name))) { + && !valid_utf8_stringz(name)) { if (msg_verbose) msg_info("%s: %s: Skipping lookup of non-UTF-8 key '%s'", myname, dict_pgsql->parser->name, name); @@ -324,11 +325,7 @@ static const char *dict_pgsql_lookup(DICT *dict, const char *name) return (0); /* do the query - set dict->error & cleanup if there's an error */ - if ((query_res = plpgsql_query(dict_pgsql, name, query, - dict_pgsql->dbname, - dict_pgsql->encoding, - dict_pgsql->username, - dict_pgsql->password)) == 0) { + if ((query_res = plpgsql_query(dict_pgsql, name, query)) == 0) { dict->error = DICT_ERR_RETRY; return 0; } @@ -404,8 +401,7 @@ static HOST *dict_pgsql_find_host(PLPGSQL *PLDB, unsigned stat, unsigned type) /* dict_pgsql_get_active - get an active connection */ -static HOST *dict_pgsql_get_active(PLPGSQL *PLDB, char *dbname, char *encoding, - char *username, char *password) +static HOST *dict_pgsql_get_active(DICT_PGSQL *dict_pgsql, PLPGSQL *PLDB) { const char *myname = "dict_pgsql_get_active"; HOST *host; @@ -414,7 +410,7 @@ static HOST *dict_pgsql_get_active(PLPGSQL *PLDB, char *dbname, char *encoding, /* try the active connections first; prefer the ones to UNIX sockets */ if ((host = dict_pgsql_find_host(PLDB, STATACTIVE, TYPEUNIX)) != NULL || (host = dict_pgsql_find_host(PLDB, STATACTIVE, TYPEINET)) != NULL || - (host = dict_pgsql_find_host(PLDB, STATACTIVE, TYPECONNSTRING)) != NULL) { + (host = dict_pgsql_find_host(PLDB, STATACTIVE, TYPECONNSTR)) != NULL) { if (msg_verbose) msg_info("%s: found active connection to host %s", myname, host->hostname); @@ -432,11 +428,11 @@ static HOST *dict_pgsql_get_active(PLPGSQL *PLDB, char *dbname, char *encoding, (host = dict_pgsql_find_host(PLDB, STATUNTRIED | STATFAIL, TYPEINET)) != NULL || (host = dict_pgsql_find_host(PLDB, STATUNTRIED | STATFAIL, - TYPECONNSTRING)) != NULL)) { + TYPECONNSTR)) != NULL)) { if (msg_verbose) msg_info("%s: attempting to connect to host %s", myname, host->hostname); - plpgsql_connect_single(host, dbname, encoding, username, password); + plpgsql_connect_single(dict_pgsql, host); if (host->stat == STATACTIVE) return host; } @@ -464,18 +460,14 @@ static void dict_pgsql_event(int unused_event, void *context) static PGSQL_RES *plpgsql_query(DICT_PGSQL *dict_pgsql, const char *name, - VSTRING *query, - char *dbname, - char *encoding, - char *username, - char *password) + VSTRING *query) { PLPGSQL *PLDB = dict_pgsql->pldb; HOST *host; PGSQL_RES *res = 0; ExecStatusType status; - while ((host = dict_pgsql_get_active(PLDB, dbname, encoding, username, password)) != NULL) { + while ((host = dict_pgsql_get_active(dict_pgsql, PLDB)) != NULL) { /* * The active host is used to escape strings in the context of the @@ -490,7 +482,7 @@ static PGSQL_RES *plpgsql_query(DICT_PGSQL *dict_pgsql, /* Check for potential dict_pgsql_quote() failure. */ if (host->stat == STATFAIL) { - plpgsql_down_host(host); + plpgsql_down_host(host, dict_pgsql->retry_interval); continue; } @@ -528,7 +520,7 @@ static PGSQL_RES *plpgsql_query(DICT_PGSQL *dict_pgsql, msg_info("dict_pgsql: successful query from host %s", host->hostname); event_request_timer(dict_pgsql_event, (void *) host, - IDLE_CONN_INTV); + dict_pgsql->idle_interval); return (res); case PGRES_FATAL_ERROR: msg_warn("pgsql query failed: fatal error from host %s: %s", @@ -559,7 +551,7 @@ static PGSQL_RES *plpgsql_query(DICT_PGSQL *dict_pgsql, */ if (res != 0) PQclear(res); - plpgsql_down_host(host); + plpgsql_down_host(host, dict_pgsql->retry_interval); } return (0); @@ -570,24 +562,25 @@ static PGSQL_RES *plpgsql_query(DICT_PGSQL *dict_pgsql, * used to reconnect to a single database when one is down or none is * connected yet. Log all errors and set the stat field of host accordingly */ -static void plpgsql_connect_single(HOST *host, char *dbname, char *encoding, char *username, char *password) +static void plpgsql_connect_single(DICT_PGSQL *dict_pgsql, HOST *host) { - if (host->type == TYPECONNSTRING) { + if (host->type == TYPECONNSTR) { host->db = PQconnectdb(host->name); } else { host->db = PQsetdbLogin(host->name, host->port, NULL, NULL, - dbname, username, password); + dict_pgsql->dbname, dict_pgsql->username, + dict_pgsql->password); } if (host->db == NULL || PQstatus(host->db) != CONNECTION_OK) { msg_warn("connect to pgsql server %s: %s", host->hostname, PQerrorMessage(host->db)); - plpgsql_down_host(host); + plpgsql_down_host(host, dict_pgsql->retry_interval); return; } - if (PQsetClientEncoding(host->db, encoding) != 0) { + if (PQsetClientEncoding(host->db, dict_pgsql->encoding) != 0) { msg_warn("dict_pgsql: cannot set the encoding to %s, skipping %s", - encoding, host->hostname); - plpgsql_down_host(host); + dict_pgsql->encoding, host->hostname); + plpgsql_down_host(host, dict_pgsql->retry_interval); return; } if (msg_verbose) @@ -611,12 +604,12 @@ static void plpgsql_close_host(HOST *host) * plpgsql_down_host - close a failed connection AND set a "stay away from * this host" timer. */ -static void plpgsql_down_host(HOST *host) +static void plpgsql_down_host(HOST *host, int retry_interval) { if (host->db) PQfinish(host->db); host->db = 0; - host->ts = time((time_t *) 0) + RETRY_CONN_INTV; + host->ts = time((time_t *) 0) + retry_interval; host->stat = STATFAIL; event_cancel_timer(dict_pgsql_event, (void *) host); } @@ -635,6 +628,10 @@ static void pgsql_parse_config(DICT_PGSQL *dict_pgsql, const char *pgsqlcf) dict_pgsql->password = cfg_get_str(p, "password", "", 0, 0); dict_pgsql->dbname = cfg_get_str(p, "dbname", "", 1, 0); dict_pgsql->encoding = cfg_get_str(p, "encoding", "UTF8", 1, 0); + dict_pgsql->retry_interval = cfg_get_int(p, "retry_interval", + DEF_RETRY_INTV, 1, 0); + dict_pgsql->idle_interval = cfg_get_int(p, "idle_interval", + DEF_IDLE_INTV, 1, 0); dict_pgsql->result_format = cfg_get_str(p, "result_format", "%s", 1, 0); /* @@ -764,7 +761,7 @@ static HOST *host_init(const char *hostname) * Modern syntax: "postgresql://connection-info". */ if (strncmp(d, "postgresql:", 11) == 0) { - host->type = TYPECONNSTRING; + host->type = TYPECONNSTR; host->name = mystrdup(d); host->port = 0; } diff --git a/src/global/dict_sqlite.c b/src/global/dict_sqlite.c index 677d05a..7d6608a 100644 --- a/src/global/dict_sqlite.c +++ b/src/global/dict_sqlite.c @@ -149,7 +149,7 @@ static const char *dict_sqlite_lookup(DICT *dict, const char *name) * Don't frustrate future attempts to make Postfix UTF-8 transparent. */ if ((dict->flags & DICT_FLAG_UTF8_ACTIVE) == 0 - && !valid_utf8_string(name, strlen(name))) { + && !valid_utf8_stringz(name)) { if (msg_verbose) msg_info("%s: %s: Skipping lookup of non-UTF-8 key '%s'", myname, dict_sqlite->parser->name, name); diff --git a/src/global/mail_addr_find.c b/src/global/mail_addr_find.c index afbccd5..c7e5545 100644 --- a/src/global/mail_addr_find.c +++ b/src/global/mail_addr_find.c @@ -442,8 +442,8 @@ const char *mail_addr_find_opt(MAPS *path, const char *address, char **extp, /* * Try localpart@ even if the domain is not local. */ - if ((strategy & MA_FIND_LOCALPART_AT) != 0 \ - &&result == 0 && path->error == 0) + if ((strategy & MA_FIND_LOCALPART_AT) != 0 + && result == 0 && path->error == 0) result = find_local(path, ratsign, 1, int_full_key, int_bare_key, query_form, extp, &saved_ext, ext_addr_buf); diff --git a/src/global/mail_date.c b/src/global/mail_date.c index 55d8907..439a0ea 100644 --- a/src/global/mail_date.c +++ b/src/global/mail_date.c @@ -10,7 +10,7 @@ /* time_t when; /* DESCRIPTION /* mail_date() converts the time specified in \fIwhen\fR to the -/* form: "Mon, 9 Dec 1996 05:38:26 -0500 (EST)" and returns +/* form: "Mon, 09 Dec 1996 05:38:26 -0500 (EST)" and returns /* a pointer to the result. The result is overwritten upon /* each call. /* DIAGNOSTICS @@ -98,8 +98,13 @@ const char *mail_date(time_t when) * First, format the date and wall-clock time. XXX The %e format (day of * month, leading zero replaced by blank) isn't in my POSIX book, but * many vendors seem to support it. + * + * The RFC 5322 Date and Time Specification recommends (i.e., should) "that + * a single space be used in each place that FWS appears". To avoid a + * potentially breaking change, we prefer the %d (two-digit day) format, + * i.e. days 1-9 now have a leading zero instead of a leading space. */ -#ifdef MISSING_STRFTIME_E +#if defined(MISSING_STRFTIME_E) || defined(TWO_DIGIT_DAY_IN_DATE_TIME) #define STRFTIME_FMT "%a, %d %b %Y %H:%M:%S " #else #define STRFTIME_FMT "%a, %e %b %Y %H:%M:%S " diff --git a/src/global/mail_dict.c b/src/global/mail_dict.c index c640a80..55ac5dc 100644 --- a/src/global/mail_dict.c +++ b/src/global/mail_dict.c @@ -52,6 +52,7 @@ #include <dict_pgsql.h> #include <dict_sqlite.h> #include <dict_memcache.h> +#include <dict_mongodb.h> #include <mail_dict.h> #include <mail_params.h> #include <mail_dict.h> @@ -71,6 +72,9 @@ static const DICT_OPEN_INFO dict_open_info[] = { #ifdef HAS_SQLITE DICT_TYPE_SQLITE, dict_sqlite_open, 0, #endif +#ifdef HAS_MONGODB + DICT_TYPE_MONGODB, dict_mongodb_open, 0, +#endif #endif /* !USE_DYNAMIC_MAPS */ DICT_TYPE_MEMCACHE, dict_memcache_open, 0, 0, diff --git a/src/global/mail_params.c b/src/global/mail_params.c index 81aee73..2a7f84c 100644 --- a/src/global/mail_params.c +++ b/src/global/mail_params.c @@ -159,6 +159,7 @@ /* char *var_maillog_file_pfxs; /* char *var_maillog_file_comp; /* char *var_maillog_file_stamp; +/* char *var_maillog_file_perms; /* char *var_postlog_service; /* /* char *var_dnssec_probe; @@ -226,6 +227,7 @@ #include <vstring_vstream.h> #include <iostuff.h> #include <midna_domain.h> +#include <logwriter.h> /* Global library. */ @@ -375,6 +377,7 @@ char *var_maillog_file; char *var_maillog_file_pfxs; char *var_maillog_file_comp; char *var_maillog_file_stamp; +char *var_maillog_file_perms; char *var_postlog_service; char *var_dnssec_probe; @@ -515,9 +518,11 @@ static void check_mail_owner(void) */ if ((pwd = getpwuid(var_owner_uid)) != 0 && strcmp(pwd->pw_name, var_mail_owner) != 0) - msg_fatal("file %s/%s: parameter %s: user %s has same user ID as %s", + msg_fatal("file %s/%s: parameter %s: user %s has the same" + " user ID %ld as user %s", var_config_dir, MAIN_CONF_FILE, - VAR_MAIL_OWNER, var_mail_owner, pwd->pw_name); + VAR_MAIL_OWNER, var_mail_owner, + (long) var_owner_uid, pwd->pw_name); } /* check_sgid_group - lookup setgid group attributes and validate */ @@ -542,9 +547,11 @@ static void check_sgid_group(void) */ if ((grp = getgrgid(var_sgid_gid)) != 0 && strcmp(grp->gr_name, var_sgid_group) != 0) - msg_fatal("file %s/%s: parameter %s: group %s has same group ID as %s", + msg_fatal("file %s/%s: parameter %s: group %s has the same" + " group ID %ld as group %s", var_config_dir, MAIN_CONF_FILE, - VAR_SGID_GROUP, var_sgid_group, grp->gr_name); + VAR_SGID_GROUP, var_sgid_group, + (long) var_sgid_gid, grp->gr_name); } /* check_overlap - disallow UID or GID sharing */ @@ -729,6 +736,7 @@ void mail_params_init() VAR_MAILLOG_FILE_PFXS, DEF_MAILLOG_FILE_PFXS, &var_maillog_file_pfxs, 1, 0, VAR_MAILLOG_FILE_COMP, DEF_MAILLOG_FILE_COMP, &var_maillog_file_comp, 1, 0, VAR_MAILLOG_FILE_STAMP, DEF_MAILLOG_FILE_STAMP, &var_maillog_file_stamp, 1, 0, + VAR_MAILLOG_FILE_PERMS, DEF_MAILLOG_FILE_PERMS, &var_maillog_file_perms, 1, 0, VAR_POSTLOG_SERVICE, DEF_POSTLOG_SERVICE, &var_postlog_service, 1, 0, VAR_DNSSEC_PROBE, DEF_DNSSEC_PROBE, &var_dnssec_probe, 0, 0, VAR_KNOWN_TCP_PORTS, DEF_KNOWN_TCP_PORTS, &var_known_tcp_ports, 0, 0, @@ -979,6 +987,9 @@ void mail_params_init() dict_db_cache_size = var_db_read_buf; dict_lmdb_map_size = var_lmdb_map_size; inet_windowsize = var_inet_windowsize; + if (set_logwriter_create_perms(var_maillog_file_perms) < 0) + msg_warn("ignoring bad permissions: %s = %s", + VAR_MAILLOG_FILE_PERMS, var_maillog_file_perms); /* * Variables whose defaults are determined at runtime, after other diff --git a/src/global/mail_params.h b/src/global/mail_params.h index 3064b01..1f03b0b 100644 --- a/src/global/mail_params.h +++ b/src/global/mail_params.h @@ -1321,6 +1321,10 @@ extern bool var_smtpd_tls_ask_ccert; #define DEF_SMTPD_TLS_RCERT 0 extern bool var_smtpd_tls_req_ccert; +#define VAR_SMTPD_TLS_ENABLE_RPK "smtpd_tls_enable_rpk" +#define DEF_SMTPD_TLS_ENABLE_RPK 0 +extern bool var_smtpd_tls_enable_rpk; + #define VAR_SMTPD_TLS_CCERT_VD "smtpd_tls_ccert_verifydepth" #define DEF_SMTPD_TLS_CCERT_VD 9 extern int var_smtpd_tls_ccert_vd; @@ -1555,6 +1559,12 @@ extern char *var_smtp_tls_mand_excl; "{md5} : {sha256}}" extern char *var_smtp_tls_fpt_dgst; +#define VAR_SMTP_TLS_ENABLE_RPK "smtp_tls_enable_rpk" +#define DEF_SMTP_TLS_ENABLE_RPK 0 +#define VAR_LMTP_TLS_ENABLE_RPK "lmtp_tls_enable_rpk" +#define DEF_LMTP_TLS_ENABLE_RPK 0 +extern bool var_smtp_tls_enable_rpk; + #define VAR_SMTP_TLS_TAFILE "smtp_tls_trust_anchor_file" #define DEF_SMTP_TLS_TAFILE "" #define VAR_LMTP_TLS_TAFILE "lmtp_tls_trust_anchor_file" @@ -1745,6 +1755,12 @@ extern bool var_smtp_sasl_enable; #define DEF_SMTP_SASL_PASSWD "" extern char *var_smtp_sasl_passwd; +#define VAR_SMTP_SASL_PASSWD_RES_DELIM "smtp_sasl_password_result_delimiter" +#define DEF_SMTP_SASL_PASSWD_RES_DELIM ":" +#define VAR_LMTP_SASL_PASSWD_RES_DELIM "lmtp_sasl_password_result_delimiter" +#define DEF_LMTP_SASL_PASSWD_RES_DELIM DEF_SMTP_SASL_PASSWD_RES_DELIM +extern char *var_smtp_sasl_passwd_res_delim; + #define VAR_SMTP_SASL_OPTS "smtp_sasl_security_options" #define DEF_SMTP_SASL_OPTS "noplaintext, noanonymous" extern char *var_smtp_sasl_opts; @@ -2437,7 +2453,7 @@ extern char *var_smtpd_exp_filter; extern bool var_smtpd_peername_lookup; #define VAR_SMTPD_FORBID_UNAUTH_PIPE "smtpd_forbid_unauth_pipelining" -#define DEF_SMTPD_FORBID_UNAUTH_PIPE 0 +#define DEF_SMTPD_FORBID_UNAUTH_PIPE 1 extern bool var_smtpd_forbid_unauth_pipe; /* @@ -3072,6 +3088,10 @@ extern bool var_disable_mime_input; #define DEF_DISABLE_MIME_OCONV 0 extern bool var_disable_mime_oconv; +#define VAR_FORCE_MIME_ICONV "force_mime_input_conversion" +#define DEF_FORCE_MIME_ICONV 0 +extern bool var_force_mime_iconv; + #define VAR_STRICT_8BITMIME "strict_8bitmime" #define DEF_STRICT_8BITMIME 0 extern bool var_strict_8bitmime; @@ -3982,6 +4002,10 @@ extern bool var_tlsp_tls_ask_ccert; #define DEF_TLSP_TLS_RCERT "$" VAR_SMTPD_TLS_RCERT extern bool var_tlsp_tls_req_ccert; +#define VAR_TLSP_TLS_ENABLE_RPK "tlsproxy_tls_enable_rpk" +#define DEF_TLSP_TLS_ENABLE_RPK "$" VAR_SMTPD_TLS_ENABLE_RPK +extern bool var_tlsp_tls_enable_rpk; + #define VAR_TLSP_TLS_CCERT_VD "tlsproxy_tls_ccert_verifydepth" #define DEF_TLSP_TLS_CCERT_VD "$" VAR_SMTPD_TLS_CCERT_VD extern int var_tlsp_tls_ccert_vd; @@ -4282,7 +4306,7 @@ extern char *var_smtpd_dns_re_filter; * Backwards compatibility. */ #define VAR_SMTPD_FORBID_BARE_LF "smtpd_forbid_bare_newline" -#define DEF_SMTPD_FORBID_BARE_LF "no" +#define DEF_SMTPD_FORBID_BARE_LF "normalize" #define VAR_SMTPD_FORBID_BARE_LF_EXCL "smtpd_forbid_bare_newline_exclusions" #define DEF_SMTPD_FORBID_BARE_LF_EXCL "$" VAR_MYNETWORKS @@ -4379,6 +4403,10 @@ extern char *var_maillog_file_comp; #define DEF_MAILLOG_FILE_STAMP "%Y%m%d-%H%M%S" extern char *var_maillog_file_stamp; +#define VAR_MAILLOG_FILE_PERMS "maillog_file_permissions" +#define DEF_MAILLOG_FILE_PERMS "0600" +extern char *var_maillog_file_perms; + #define VAR_POSTLOG_SERVICE "postlog_service_name" #define DEF_POSTLOG_SERVICE MAIL_SERVICE_POSTLOG extern char *var_postlog_service; diff --git a/src/global/mail_proto.h b/src/global/mail_proto.h index 315a2e1..bea0886 100644 --- a/src/global/mail_proto.h +++ b/src/global/mail_proto.h @@ -63,6 +63,13 @@ #define MAIL_SERVICE_POSTLOG "postlog" /* + * Process names: convention is to use the basename of an executable file, + * but there is nothing to enforce that. + */ +#define MAIL_PROC_NAME_SMTP "smtp" +#define MAIL_PROC_NAME_LMTP "lmtp" + + /* * Mail source classes. Used to specify policy decisions for content * inspection and SMTPUTF8 detection. */ diff --git a/src/global/mail_version.h b/src/global/mail_version.h index 9eda667..9e08896 100644 --- a/src/global/mail_version.h +++ b/src/global/mail_version.h @@ -20,8 +20,8 @@ * Patches change both the patchlevel and the release date. Snapshots have no * patchlevel; they change the release date only. */ -#define MAIL_RELEASE_DATE "20240304" -#define MAIL_VERSION_NUMBER "3.8.6" +#define MAIL_RELEASE_DATE "20240306" +#define MAIL_VERSION_NUMBER "3.9" #ifdef SNAPSHOT #define MAIL_VERSION_DATE "-" MAIL_RELEASE_DATE diff --git a/src/global/maillog_client.c b/src/global/maillog_client.c index 7f79a1f..34952ef 100644 --- a/src/global/maillog_client.c +++ b/src/global/maillog_client.c @@ -58,7 +58,7 @@ /* unitialized and the process environment does not specify /* POSTLOG_SERVICE, the program will log to the syslog service /* instead. -/* .IP "myhostname (default: see postconf -d output)" +/* .IP "myhostname (default: see 'postconf -d' output)" /* The internet hostname of this mail system. /* .IP "postlog_service_name (postlog)" /* The name of the internal postlog logging service. diff --git a/src/global/maps.c b/src/global/maps.c index 790396b..d237002 100644 --- a/src/global/maps.c +++ b/src/global/maps.c @@ -195,8 +195,12 @@ const char *maps_find(MAPS *maps, const char *name, int flags) for (map_name = maps->argv->argv; *map_name; map_name++) { if ((dict = dict_handle(*map_name)) == 0) msg_panic("%s: dictionary not found: %s", myname, *map_name); - if (flags != 0 && (dict->flags & flags) == 0) + if (flags != 0 && (dict->flags & flags) == 0) { + if (msg_verbose) + msg_info("%s: %s: skipping %s lookup for %s", + myname, maps->title, *map_name, name); continue; + } if ((expansion = dict_get(dict, name)) != 0) { if (*expansion == 0) { msg_warn("%s lookup of %s returns an empty string result", @@ -252,8 +256,12 @@ const char *maps_file_find(MAPS *maps, const char *name, int flags) if ((dict->flags & DICT_FLAG_SRC_RHS_IS_FILE) == 0) msg_panic("%s: %s: opened without DICT_FLAG_SRC_RHS_IS_FILE", myname, maps->title); - if (flags != 0 && (dict->flags & flags) == 0) + if (flags != 0 && (dict->flags & flags) == 0) { + if (msg_verbose) + msg_info("%s: %s: skipping %s lookup for %s", + myname, maps->title, *map_name, name); continue; + } if ((expansion = dict_get(dict, name)) != 0) { if (*expansion == 0) { msg_warn("%s lookup of %s returns an empty string result", diff --git a/src/global/wildcard_inet_addr.c b/src/global/wildcard_inet_addr.c index 97f6c46..0a3c37a 100644 --- a/src/global/wildcard_inet_addr.c +++ b/src/global/wildcard_inet_addr.c @@ -11,7 +11,7 @@ /* wildcard_inet_addr() determines all wild-card addresses /* for all supported address families. /* DIAGNOSTICS -/* Fatal errors: out of memory. +/* Fatal errors: out of memory; no wildcard addresses. /* SEE ALSO /* inet_addr_list(3) address list management /* LICENSE diff --git a/src/local/command.c b/src/local/command.c index 4781daf..368307d 100644 --- a/src/local/command.c +++ b/src/local/command.c @@ -17,7 +17,8 @@ /* Duplicate commands for the same recipient are suppressed. /* A limited amount of information is exported via the environment: /* HOME, SHELL, LOGNAME, USER, EXTENSION, DOMAIN, RECIPIENT (entire -/* address) LOCAL (just the local part) and SENDER. The exported +/* address) LOCAL (just the local part), SENDER, and ENVID +/* (see RFC 3461). The exported /* information is censored with var_cmd_filter. /* /* Arguments: @@ -169,6 +170,8 @@ int deliver_command(LOCAL_STATE state, USER_ATTR usr_attr, const char *comma if (state.msg_attr.rcpt.orig_addr && state.msg_attr.rcpt.orig_addr[0]) argv_add(env, "ORIGINAL_RECIPIENT", state.msg_attr.rcpt.orig_addr, ARGV_END); + if (state.request->dsn_envid[0]) + argv_add(env, "ENVID", state.request->dsn_envid, ARGV_END); #define EXPORT_REQUEST(name, value) \ if ((value)[0]) argv_add(env, (name), (value), ARGV_END); diff --git a/src/local/local.c b/src/local/local.c index 32bdea7..10b8082 100644 --- a/src/local/local.c +++ b/src/local/local.c @@ -207,27 +207,30 @@ /* is specified with the \fBcommand_expansion_filter\fR configuration /* parameter. /* .IP \fBSHELL\fR -/* The recipient user's login shell. +/* The envelope recipient user's login shell. /* .IP \fBHOME\fR -/* The recipient user's home directory. +/* The envelope recipient user's home directory. /* .IP \fBUSER\fR -/* The bare recipient name. +/* The bare envelope recipient name. /* .IP \fBEXTENSION\fR -/* The optional recipient address extension. +/* The optional envelope recipient address extension. /* .IP \fBDOMAIN\fR -/* The recipient address domain part. +/* The envelope recipient address domain part. /* .IP \fBLOGNAME\fR -/* The bare recipient name. +/* The bare envelope recipient name. /* .IP \fBLOCAL\fR -/* The entire recipient address localpart (text to the left of the -/* rightmost @ character). +/* The entire envelope recipient address localpart (text to +/* the left of the rightmost @ character). /* .IP \fBORIGINAL_RECIPIENT\fR -/* The entire recipient address, before any address rewriting -/* or aliasing (Postfix 2.5 and later). +/* The entire envelope recipient address, before any address +/* rewriting or aliasing (Postfix 2.5 and later). /* .IP \fBRECIPIENT\fR -/* The entire recipient address. +/* The entire envelope recipient address. /* .IP \fBSENDER\fR -/* The entire sender address. +/* The entire envelope sender address. +/* .IP \fBENVID\fR +/* The optional RFC 3461 envelope ID. Available as of Postfix +/* 3.9. /* .PP /* Additional remote client information is made available via /* the following environment variables: @@ -413,7 +416,9 @@ /* home_mailbox, mail_spool_directory, fallback_transport_maps, /* fallback_transport, and luser_relay. /* .IP "\fBalias_maps (see 'postconf -d' output)\fR" -/* The alias databases that are used for \fBlocal\fR(8) delivery. +/* Optional lookup tables with aliases that apply only to \fBlocal\fR(8) +/* recipients; this is unlike virtual_alias_maps that apply to all +/* recipients: \fBlocal\fR(8), virtual, and remote. /* .IP "\fBforward_path (see 'postconf -d' output)\fR" /* The \fBlocal\fR(8) delivery agent search list for finding a .forward /* file with user-specified delivery methods. diff --git a/src/master/master.c b/src/master/master.c index 1fc3fe9..b6afe3f 100644 --- a/src/master/master.c +++ b/src/master/master.c @@ -135,13 +135,13 @@ /* The external command to execute when a Postfix daemon program is /* invoked with the -D option. /* .IP "\fBinet_interfaces (all)\fR" -/* The network interface addresses that this mail system receives -/* mail on. -/* .IP "\fBinet_protocols (see 'postconf -d output')\fR" +/* The local network interface addresses that this mail system +/* receives mail on. +/* .IP "\fBinet_protocols (see 'postconf -d' output)\fR" /* The Internet protocols Postfix will attempt to use when making /* or accepting connections. /* .IP "\fBimport_environment (see 'postconf -d' output)\fR" -/* The list of environment parameters that a privileged Postfix +/* The list of environment variables that a privileged Postfix /* process will import from a non-Postfix parent process, or name=value /* environment overrides. /* .IP "\fBmail_owner (postfix)\fR" @@ -495,7 +495,7 @@ int main(int argc, char **argv) vstring_sprintf(lock_path, "%s/%s.pid", DEF_PID_DIR, var_procname); if (test_lock && access(vstring_str(lock_path), F_OK) < 0) exit(0); - lock_fp = open_lock(vstring_str(lock_path), O_RDWR | O_CREAT, 0644, why); + lock_fp = open_lock(vstring_str(lock_path), O_RDWR | O_CREAT, 0600, why); if (test_lock) exit(lock_fp ? 0 : 1); if (lock_fp == 0) @@ -513,7 +513,7 @@ int main(int argc, char **argv) vstring_sprintf(data_lock_path, "%s/%s.lock", var_data_dir, var_procname); set_eugid(var_owner_uid, var_owner_gid); data_lock_fp = - open_lock(vstring_str(data_lock_path), O_RDWR | O_CREAT, 0644, why); + open_lock(vstring_str(data_lock_path), O_RDWR | O_CREAT, 0600, why); set_ugid(getuid(), getgid()); if (data_lock_fp == 0) msg_fatal("open lock file %s: %s", diff --git a/src/master/master_ent.c b/src/master/master_ent.c index 5edc308..98f8404 100644 --- a/src/master/master_ent.c +++ b/src/master/master_ent.c @@ -369,8 +369,12 @@ MASTER_SERV *get_master_ent() } else { MASTER_INET_ADDRLIST(serv) = strcasecmp(saved_interfaces, INET_INTERFACES_ALL) ? - own_inet_addr_list() : /* virtual */ - wildcard_inet_addr_list(); /* wild-card */ + own_inet_addr_list() : /* result can be empty */ + wildcard_inet_addr_list(); /* result can't be empty */ + if (MASTER_INET_ADDRLIST(serv)->used == 0) + fatal_with_context("service definition requires valid" + " host name or address, or non-empty" + " %s setting", VAR_INET_INTERFACES); inet_addr_list_uniq(MASTER_INET_ADDRLIST(serv)); serv->listen_fd_count = MASTER_INET_ADDRLIST(serv)->used; } diff --git a/src/oqmgr/qmgr_deliver.c b/src/oqmgr/qmgr_deliver.c index 03e0340..6c09350 100644 --- a/src/oqmgr/qmgr_deliver.c +++ b/src/oqmgr/qmgr_deliver.c @@ -155,7 +155,7 @@ static int qmgr_deliver_send_request(QMGR_ENTRY *entry, VSTREAM *stream) */ for (recipient = list.info; recipient < list.info + list.len; recipient++) if (var_smtputf8_enable && (addr = recipient->address)[0] - && !allascii(addr) && valid_utf8_string(addr, strlen(addr))) { + && !allascii(addr) && valid_utf8_stringz(addr)) { smtputf8 |= SMTPUTF8_FLAG_RECIPIENT; if (message->verp_delims) smtputf8 |= SMTPUTF8_FLAG_SENDER; @@ -334,7 +334,7 @@ static void qmgr_deliver_update(int unused_event, void *context) #define SUSPENDED "delivery temporarily suspended: " if (status == DELIVER_STAT_CRASH) - DSN_SIMPLE(&dsb->dsn, "4.3.0", "unknown mail transport error"); + (void) DSN_SIMPLE(&dsb->dsn, "4.3.0", "unknown mail transport error"); if (status == DELIVER_STAT_CRASH || status == DELIVER_STAT_DEFER) { message->flags |= DELIVER_STAT_DEFER; if (VSTRING_LEN(dsb->status)) { diff --git a/src/pipe/pipe.c b/src/pipe/pipe.c index 8a99430..3017937 100644 --- a/src/pipe/pipe.c +++ b/src/pipe/pipe.c @@ -230,6 +230,11 @@ /* This information is modified by the \fBh\fR flag for case folding. /* .sp /* This feature is available as of Postfix 2.5. +/* .IP \fB${envid}\fR +/* This macro expands to the RFC 3461 envelope ID if available, +/* otherwise the empty string. +/* .sp +/* This feature is available as of Postfix 3.9. /* .IP \fB${extension}\fR /* This macro expands to the extension part of a recipient address. /* For example, with an address \fIuser+foo@domain\fR the extension is @@ -544,6 +549,7 @@ #define PIPE_DICT_SASL_USERNAME "sasl_username" /* key */ #define PIPE_DICT_SASL_SENDER "sasl_sender" /* key */ #define PIPE_DICT_QUEUE_ID "queue_id" /* key */ +#define PIPE_DICT_ENVID "envid" /* key */ /* * Flags used to pass back the type of special parameter found by @@ -649,6 +655,7 @@ static int parse_callback(int type, VSTRING *buf, void *context) PIPE_DICT_SASL_USERNAME, 0, PIPE_DICT_SASL_SENDER, 0, PIPE_DICT_QUEUE_ID, 0, + PIPE_DICT_ENVID, 0, 0, 0, }; struct cmd_flags *p; @@ -1278,6 +1285,8 @@ static int deliver_message(DELIVER_REQUEST *request, char *service, char **argv) request->sasl_sender); dict_update(PIPE_DICT_TABLE, PIPE_DICT_QUEUE_ID, request->queue_id); + dict_update(PIPE_DICT_TABLE, PIPE_DICT_ENVID, + request->dsn_envid); vstring_free(buf); if ((expanded_argv = expand_argv(service, attr.command, diff --git a/src/postalias/postalias.c b/src/postalias/postalias.c index d17e397..72ca729 100644 --- a/src/postalias/postalias.c +++ b/src/postalias/postalias.c @@ -393,7 +393,7 @@ static void postalias(char *map_type, char *path_name, int postalias_flags, */ if ((mkmap->dict->flags & DICT_FLAG_UTF8_ACTIVE) && !allascii(STR(line_buffer)) - && !valid_utf8_string(STR(line_buffer), LEN(line_buffer))) { + && !valid_utf8_stringz(STR(line_buffer))) { msg_warn("%s, line %d: non-UTF-8 input \"%s\"" " -- ignoring this line", VSTREAM_PATH(source_fp), lineno, STR(line_buffer)); diff --git a/src/postcat/postcat.c b/src/postcat/postcat.c index 36f2740..26b46a9 100644 --- a/src/postcat/postcat.c +++ b/src/postcat/postcat.c @@ -76,7 +76,7 @@ /* The default location of the Postfix main.cf and master.cf /* configuration files. /* .IP "\fBimport_environment (see 'postconf -d' output)\fR" -/* The list of environment parameters that a privileged Postfix +/* The list of environment variables that a privileged Postfix /* process will import from a non-Postfix parent process, or name=value /* environment overrides. /* .IP "\fBqueue_directory (see 'postconf -d' output)\fR" @@ -274,7 +274,7 @@ static void postcat(VSTREAM *fp, VSTRING *buffer, int flags) break; /* Optimization: skip to extracted segment marker. */ if (do_print == 0 && (flags & PC_FLAG_PRINT_ENV) - && data_offset >= 0 && data_size >= 0 + && data_offset > 0 && data_size >= 0 && vstream_fseek(fp, data_offset + data_size, SEEK_SET) < 0) msg_fatal("seek error: %m"); } @@ -289,7 +289,7 @@ static void postcat(VSTREAM *fp, VSTRING *buffer, int flags) PRINT_MARKER(flags, fp, offset, rec_type, "MESSAGE CONTENTS"); /* Optimization: skip to extracted segment marker. */ if ((flags & PC_MASK_PRINT_TEXT) == 0 - && data_offset >= 0 && data_size >= 0 + && data_offset > 0 && data_size >= 0 && vstream_fseek(fp, data_offset + data_size, SEEK_SET) < 0) msg_fatal("seek error: %m"); /* Update the state machine, even when skipping. */ diff --git a/src/postconf/Makefile.in b/src/postconf/Makefile.in index 6aff794..6df6dfa 100644 --- a/src/postconf/Makefile.in +++ b/src/postconf/Makefile.in @@ -17,7 +17,7 @@ MAKES = bool_table.h bool_vars.h int_table.h int_vars.h str_table.h \ nint_table.h nint_vars.h nbool_table.h nbool_vars.h long_table.h \ long_vars.h str_fn_table.h str_fn_vars.h DB_MAKES= pcf_ldap_suffixes.h pcf_memcache_suffixes.h pcf_mysql_suffixes.h \ - pcf_pgsql_suffixes.h pcf_sqlite_suffixes.h + pcf_pgsql_suffixes.h pcf_sqlite_suffixes.h pcf_mongodb_suffixes.h TEST_TMP= main.cf master.cf test*.tmp DUMMIES = makes_dummy # for "make -j" PROG = postconf @@ -55,7 +55,8 @@ tests: test1 test2 test3 test4 test5 test6 test7 test8 test9 test10 test11 \ test31 test32 test33 test34 test35 test36 test37 test39 test40 test41 \ test42 test43 test44 test45 test46 test47 test48 test49 test50 test51 \ test52 test53 test54 test55 test56 test57 test58 test59 test60 test61 \ - test62 test63 test64 test65 test66 test67 test68 test69 test70 test71 + test62 test63 test64 test65 test66 test67 test68 test69 test70 test71 \ + test72 test73 test74 test75 test76 root_tests: @@ -78,6 +79,9 @@ pcf_ldap_suffixes.h: ../global/dict_ldap.c pcf_memcache_suffixes.h: ../global/dict_memcache.c sh extract_cfg.sh -d ../global/dict_memcache.c > $@ +pcf_mongodb_suffixes.h: ../global/dict_mongodb.c + sh extract_cfg.sh -d ../global/dict_mongodb.c > $@ + pcf_mysql_suffixes.h: ../global/dict_mysql.c sh extract_cfg.sh -d -s ../global/dict_mysql.c > $@ @@ -465,6 +469,9 @@ test29: $(PROG) test29.ref echo 'memcachexx = proxy:memcache:memcachefoo' >> main.cf echo 'memcachefoo_domain = bar' >> main.cf echo 'memcachefoo_domainx = bar' >> main.cf + echo 'mongodbxx = proxy:mongodb:mongodbfoo' >> main.cf + echo 'mongodbfoo_domain = bar' >> main.cf + echo 'mongodbfoo_domainx = bar' >> main.cf touch -t 197101010000 main.cf $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -nc . >test29.tmp 2>&1 diff test29.ref test29.tmp @@ -807,7 +814,7 @@ test58: $(PROG) test58.ref echo 'yy_backup = bbb' >> main.cf echo 'yy_bogus = bbb' >> main.cf touch -t 197101010000 main.cf - $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./postconf -nc. >test58.tmp 2>&1 || true + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -nc. >test58.tmp 2>&1 || true diff test58.ref test58.tmp rm -f main.cf master.cf test58.tmp @@ -989,6 +996,75 @@ test71: $(PROG) test71.ref diff test71.ref test71.tmp rm -f main.cf master.cf test71.tmp +# Different requests to add lines to master.cf. +test72: $(PROG) test72.ref + rm -f main.cf master.cf + touch main.cf master.cf + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. smtp/unix='smtp unix - n n - 0 other' + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. smtp/abcd='smtp fifo - n n - 0 other' + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. smtp/abcd='smtp inet - n n - 0 other' + touch -t 197201010000 main.cf + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. >test72.tmp 2>&1 + diff test72.ref test72.tmp + rm -f main.cf master.cf test72.tmp + +# Replace one entry based on the name+type in the request's service entry. +test73: $(PROG) test73.ref + rm -f main.cf master.cf + touch main.cf master.cf + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. smtp/unix='smtp unix - n n - 0 other' + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. smtp/abcd='smtp fifo - n n - 0 other' + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. smtp/abcd='smtp inet - n n - 0 other' + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. smtp/abcd='smtp unix - n n - 0 otherx' + touch -t 197301010000 main.cf + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. >test73.tmp 2>&1 + diff test73.ref test73.tmp + rm -f main.cf master.cf test73.tmp + +# Replace one entry based on the name+type in the request's service pattern. +test74: $(PROG) test74.ref + rm -f main.cf master.cf + touch main.cf master.cf + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. smtp/unix='smtp unix - n n - 0 other' + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. smtp/abcd='smtp fifo - n n - 0 other' + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. smtp/abcd='smtp inet - n n - 0 other' + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. smtp/fifo='lmtp unix - n n - 0 otherx' + touch -t 197401010000 main.cf + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -Mc. >test74.tmp 2>&1 + diff test74.ref test74.tmp + rm -f main.cf master.cf test74.tmp + +# Warn about skipping redundant name=value update. +test75: $(PROG) test75.ref + rm -f main.cf master.cf + touch main.cf master.cf + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -c. mail_version=x mail_version=y >test75.tmp 2>&1 + touch -t 197501010000 main.cf + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -nc. >>test75.tmp 2>&1 + diff test75.ref test75.tmp + rm -f main.cf master.cf test75.tmp + +# Warn about unused, deprecated, or deleted parameters. +test76: $(PROG) test76.ref + rm -f main.cf master.cf + touch main.cf master.cf + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -c. \ + config_directory=. \ + deleted-test-only=whatever \ + disable_dns_lookups=no \ + lmtp_use_tls=no \ + smtpd_tls_dh1024_param_file=auto >test76.tmp 2>&1 + touch -t 197601010000 main.cf + echo foo unix - n n - 0 other >> master.cf + echo ' -o alias_maps=foo' >> master.cf + echo ' -o smtp_enforce_tls=yes' >> master.cf + touch -t 197601010000 master.cf + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -nc. >>test76.tmp 2>&1 + diff test76.ref test76.tmp + $(HTABLE_FIX) $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -qnc. >/dev/null 2>test76.tmp + diff /dev/null test76.tmp + rm -f main.cf master.cf test76.tmp + printfck: $(OBJS) $(PROG) rm -rf printfck mkdir printfck @@ -1094,6 +1170,7 @@ postconf_dbms.o: ../../include/dict.h postconf_dbms.o: ../../include/dict_ht.h postconf_dbms.o: ../../include/dict_ldap.h postconf_dbms.o: ../../include/dict_memcache.h +postconf_dbms.o: ../../include/dict_mongodb.h postconf_dbms.o: ../../include/dict_mysql.h postconf_dbms.o: ../../include/dict_pcre.h postconf_dbms.o: ../../include/dict_pgsql.h @@ -1118,6 +1195,7 @@ postconf_dbms.o: ../../include/vstream.h postconf_dbms.o: ../../include/vstring.h postconf_dbms.o: pcf_ldap_suffixes.h postconf_dbms.o: pcf_memcache_suffixes.h +postconf_dbms.o: pcf_mongodb_suffixes.h postconf_dbms.o: pcf_mysql_suffixes.h postconf_dbms.o: pcf_pgsql_suffixes.h postconf_dbms.o: pcf_sqlite_suffixes.h @@ -1184,6 +1262,7 @@ postconf_main.o: postconf_main.c postconf_master.o: ../../include/argv.h postconf_master.o: ../../include/check_arg.h postconf_master.o: ../../include/dict.h +postconf_master.o: ../../include/dict_ht.h postconf_master.o: ../../include/htable.h postconf_master.o: ../../include/mail_params.h postconf_master.o: ../../include/master_proto.h diff --git a/src/postconf/postconf.c b/src/postconf/postconf.c index f598a5b..74f13b2 100644 --- a/src/postconf/postconf.c +++ b/src/postconf/postconf.c @@ -8,7 +8,7 @@ /* .ti -4 /* \fBManaging main.cf:\fR /* -/* \fBpostconf\fR [\fB-dfhHnopvx\fR] [\fB-c \fIconfig_dir\fR] +/* \fBpostconf\fR [\fB-dfhHnopqvx\fR] [\fB-c \fIconfig_dir\fR] /* [\fB-C \fIclass,...\fR] [\fIparameter ...\fR] /* /* \fBpostconf\fR [\fB-epv\fR] [\fB-c \fIconfig_dir\fR] @@ -23,7 +23,7 @@ /* .ti -4 /* \fBManaging master.cf service entries:\fR /* -/* \fBpostconf\fR \fB-M\fR [\fB-fovx\fR] [\fB-c \fIconfig_dir\fR] +/* \fBpostconf\fR \fB-M\fR [\fB-foqvx\fR] [\fB-c \fIconfig_dir\fR] /* [\fIservice\fR[\fB/\fItype\fR]\fI ...\fR] /* /* \fBpostconf\fR \fB-M\fR [\fB-ev\fR] [\fB-c \fIconfig_dir\fR] @@ -38,7 +38,7 @@ /* .ti -4 /* \fBManaging master.cf service fields:\fR /* -/* \fBpostconf\fR \fB-F\fR [\fB-fhHovx\fR] [\fB-c \fIconfig_dir\fR] +/* \fBpostconf\fR \fB-F\fR [\fB-fhHoqvx\fR] [\fB-c \fIconfig_dir\fR] /* [\fIservice\fR[\fB/\fItype\fR[\fB/\fIfield\fR]]\fI ...\fR] /* /* \fBpostconf\fR \fB-F\fR [\fB-ev\fR] [\fB-c \fIconfig_dir\fR] @@ -47,7 +47,7 @@ /* .ti -4 /* \fBManaging master.cf service parameters:\fR /* -/* \fBpostconf\fR \fB-P\fR [\fB-fhHovx\fR] [\fB-c \fIconfig_dir\fR] +/* \fBpostconf\fR \fB-P\fR [\fB-fhHoqvx\fR] [\fB-c \fIconfig_dir\fR] /* [\fIservice\fR[\fB/\fItype\fR[\fB/\fIparameter\fR]]\fI ...\fR] /* /* \fBpostconf\fR \fB-P\fR [\fB-ev\fR] [\fB-c \fIconfig_dir\fR] @@ -293,6 +293,11 @@ /* \fBmemcache_table\fR(5). /* /* This feature is available with Postfix 2.9 and later. +/* .IP "\fBmongodb\fR" +/* MongoDB database client. This is described in +/* \fBmongodb_table\fR(5). +/* +/* This feature is available with Postfix 3.9 and later. /* .IP "\fBmysql\fR (read-only)" /* MySQL database client. Available on systems with support /* for MySQL databases. This is described in \fBmysql_table\fR(5). @@ -452,6 +457,10 @@ /* wildcard fields. /* /* This feature is available with Postfix 2.11 and later. +/* .IP \fB-q\fR +/* Do not log warnings for deprecated or unused parameters. +/* +/* This feature is available with Postfix 3.9 and later. /* .IP "\fB-t\fR [\fItemplate_file\fR]" /* Display the templates for text that appears at the beginning /* of delivery status notification (DSN) messages, without @@ -779,6 +788,8 @@ static void pcf_check_compat_options(int optval) const int (*op)[2]; int excess; + optval &= ~PCF_DEF_MODE; + for (op = pcf_compat_options; op[0][0] != 0; op++) { if ((optval & *op[0]) != 0 && (excess = (optval & ~((*op)[0] | (*op)[1]))) != 0) @@ -844,7 +855,7 @@ int main(int argc, char **argv) /* * Parse JCL. */ - while ((ch = GETOPT(argc, argv, "aAbc:C:deEfFhHlmMno:pPtT:vxX#")) > 0) { + while ((ch = GETOPT(argc, argv, "aAbc:C:deEfFhHlmMno:pPqtT:vxX#")) > 0) { switch (ch) { case 'a': pcf_cmd_mode |= PCF_SHOW_SASL_SERV; @@ -912,6 +923,9 @@ int main(int argc, char **argv) case 'P': pcf_cmd_mode |= PCF_MASTER_PARAM; break; + case 'q': + pcf_cmd_mode &= ~(PCF_WARN_UNUSED_DEPRECATED); + break; case 't': pcf_cmd_mode |= PCF_DUMP_DSN_TEMPL; if (ext_argv) @@ -1028,7 +1042,7 @@ int main(int argc, char **argv) pcf_set_parameters(override_params->argv); pcf_register_builtin_parameters(basename(argv[0]), getpid()); pcf_register_service_parameters(); - pcf_register_user_parameters(); + pcf_register_user_parameters(pcf_cmd_mode); if (pcf_cmd_mode & PCF_MASTER_FLD) pcf_show_master_fields(VSTREAM_OUT, pcf_cmd_mode, argc - optind, argv + optind); @@ -1038,7 +1052,8 @@ int main(int argc, char **argv) else pcf_show_master_entries(VSTREAM_OUT, pcf_cmd_mode, argc - optind, argv + optind); - pcf_flag_unused_master_parameters(); + if (pcf_cmd_mode & PCF_WARN_UNUSED_DEPRECATED) + pcf_flag_unused_master_parameters(); } /* @@ -1090,7 +1105,7 @@ int main(int argc, char **argv) pcf_read_master(PCF_WARN_ON_OPEN_ERROR); pcf_register_service_parameters(); if ((pcf_cmd_mode & PCF_SHOW_DEFS) == 0) - pcf_register_user_parameters(); + pcf_register_user_parameters(pcf_cmd_mode); /* * Show the requested values. @@ -1099,11 +1114,12 @@ int main(int argc, char **argv) argv + optind); /* - * Flag unused parameters. This makes no sense with "postconf -d", - * because that ignores all the user-specified parameters and - * user-specified macro expansions in main.cf. + * Flag unused or deprecated parameters. This makes no sense with + * "postconf -d", because that ignores all the user-specified + * parameters and user-specified macro expansions in main.cf. */ - if ((pcf_cmd_mode & PCF_SHOW_DEFS) == 0) { + if ((pcf_cmd_mode & PCF_SHOW_DEFS) == 0 + && (pcf_cmd_mode & PCF_WARN_UNUSED_DEPRECATED) != 0) { pcf_flag_unused_main_parameters(); pcf_flag_unused_master_parameters(); } diff --git a/src/postconf/postconf.h b/src/postconf/postconf.h index 24a1ed7..b42245c 100644 --- a/src/postconf/postconf.h +++ b/src/postconf/postconf.h @@ -46,8 +46,9 @@ #define PCF_MASTER_PARAM (1<<19) /* manage master.cf -o name=value */ #define PCF_HIDE_VALUE (1<<20) /* hide main.cf/master.cf =value */ #define PCF_SHOW_TLS (1<<21) /* TLS support introspection */ +#define PCF_WARN_UNUSED_DEPRECATED (1<<22) /* As the name says */ -#define PCF_DEF_MODE 0 +#define PCF_DEF_MODE (PCF_WARN_UNUSED_DEPRECATED) /* * Structure for one "valid parameter" (built-in, service-defined or valid @@ -274,12 +275,12 @@ typedef struct { /* * postconf_user.c. */ -extern void pcf_register_user_parameters(void); +extern void pcf_register_user_parameters(int); /* * postconf_dbms.c */ -extern void pcf_register_dbms_parameters(const char *, +extern void pcf_register_dbms_parameters(int, const char *, const char *(*) (const char *, int, PCF_MASTER_ENT *), PCF_MASTER_ENT *); diff --git a/src/postconf/postconf_dbms.c b/src/postconf/postconf_dbms.c index 0ed5b53..105ae85 100644 --- a/src/postconf/postconf_dbms.c +++ b/src/postconf/postconf_dbms.c @@ -6,8 +6,9 @@ /* SYNOPSIS /* #include <postconf.h> /* -/* void pcf_register_dbms_parameters(param_value, flag_parameter, +/* void pcf_register_dbms_parameters(mode, param_value, flag_parameter, /* local_scope) +/* int mode; /* const char *param_value; /* const char *(flag_parameter) (const char *, int, PCF_MASTER_ENT *); /* PCF_MASTER_ENT *local_scope; @@ -17,6 +18,9 @@ /* the database name to a database-defined suffix. /* /* Arguments: +/* .IP mode +/* If PCF_WARN_UNUSED_DEPRECATED is set, warn about unused +/* database settings. /* .IP param_value /* A parameter value to be searched for "type:table" strings. /* When a database type is found that supports legacy-style @@ -77,6 +81,7 @@ #include <dict_pgsql.h> #include <dict_sqlite.h> #include <dict_memcache.h> +#include <dict_mongodb.h> #include <dict_regexp.h> #include <dict_pcre.h> @@ -131,6 +136,13 @@ static const char *pcf_memcache_suffixes[] = { 0, }; +/* See mongodb_table(5). */ + +static const char *pcf_mongodb_suffixes[] = { +#include "pcf_mongodb_suffixes.h" + 0, +}; + /* * Bundle up the database types and their suffix lists. */ @@ -149,6 +161,7 @@ static const PCF_DBMS_INFO pcf_dbms_info[] = { {DICT_TYPE_PGSQL, PCF_DBMS_CLASS_CLIENT, pcf_pgsql_suffixes}, {DICT_TYPE_SQLITE, PCF_DBMS_CLASS_CLIENT, pcf_sqlite_suffixes}, {DICT_TYPE_MEMCACHE, PCF_DBMS_CLASS_CLIENT, pcf_memcache_suffixes}, + {DICT_TYPE_MONGODB, PCF_DBMS_CLASS_CLIENT, pcf_mongodb_suffixes}, {DICT_TYPE_REGEXP, PCF_DBMS_CLASS_REGEX}, {DICT_TYPE_PCRE, PCF_DBMS_CLASS_REGEX}, {0}, @@ -163,7 +176,8 @@ static const PCF_DBMS_INFO pcf_dbms_info[] = { /* pcf_check_dbms_client - look for unused names in client configuration */ -static void pcf_check_dbms_client(const PCF_DBMS_INFO *dp, const char *cf_file) +static void pcf_check_dbms_client(int mode, const PCF_DBMS_INFO *dp, + const char *cf_file) { DICT *dict; VSTREAM *fp; @@ -217,19 +231,21 @@ static void pcf_check_dbms_client(const PCF_DBMS_INFO *dp, const char *cf_file) * code, because a database client parameter namespace is unlike the * parameter namespaces in main.cf or master.cf. */ - for (cpp = dp->db_suffixes; *cpp; cpp++) - (void) dict_del(dict, *cpp); - for (dir = DICT_SEQ_FUN_FIRST; - dict->sequence(dict, dir, &name, &value) == DICT_STAT_SUCCESS; - dir = DICT_SEQ_FUN_NEXT) - msg_warn("%s: unused parameter: %s=%s", dict_spec, name, value); + if (mode & PCF_WARN_UNUSED_DEPRECATED) { + for (cpp = dp->db_suffixes; *cpp; cpp++) + (void) dict_del(dict, *cpp); + for (dir = DICT_SEQ_FUN_FIRST; + dict->sequence(dict, dir, &name, &value) == DICT_STAT_SUCCESS; + dir = DICT_SEQ_FUN_NEXT) + msg_warn("%s: unused parameter: %s=%s", dict_spec, name, value); + } } myfree(dict_spec); } /* pcf_register_dbms_helper - parse one possible database type:name */ -static void pcf_register_dbms_helper(char *str_value, +static void pcf_register_dbms_helper(int mode, char *str_value, const char *(flag_parameter) (const char *, int, PCF_MASTER_ENT *), PCF_MASTER_ENT *local_scope, int recurse) @@ -258,8 +274,8 @@ static void pcf_register_dbms_helper(char *str_value, myfree(err); } if (recurse) - pcf_register_dbms_helper(db_type, flag_parameter, local_scope, - recurse); + pcf_register_dbms_helper(mode, db_type, flag_parameter, + local_scope, recurse); continue; } @@ -287,7 +303,7 @@ static void pcf_register_dbms_helper(char *str_value, for (dp = pcf_dbms_info; dp->db_type != 0; dp++) { if (strcmp(db_type, dp->db_type) == 0) { if (dp->db_class == PCF_DBMS_CLASS_CLIENT) - pcf_check_dbms_client(dp, prefix); + pcf_check_dbms_client(mode, dp, prefix); break; } } @@ -321,8 +337,8 @@ static void pcf_register_dbms_helper(char *str_value, break; } } - pcf_register_dbms_helper(prefix, flag_parameter, local_scope, - next_recurse); + pcf_register_dbms_helper(mode, prefix, flag_parameter, + local_scope, next_recurse); continue; } else { for (dp = pcf_dbms_info; dp->db_type != 0; dp++) { @@ -347,7 +363,7 @@ static void pcf_register_dbms_helper(char *str_value, /* pcf_register_dbms_parameters - look for database_type:prefix_name */ -void pcf_register_dbms_parameters(const char *param_value, +void pcf_register_dbms_parameters(int mode, const char *param_value, const char *(flag_parameter) (const char *, int, PCF_MASTER_ENT *), PCF_MASTER_ENT *local_scope) { @@ -363,7 +379,8 @@ void pcf_register_dbms_parameters(const char *param_value, buffer = vstring_alloc(100); bufp = pcf_expand_parameter_value(buffer, PCF_SHOW_EVAL, param_value, local_scope); - pcf_register_dbms_helper(bufp, flag_parameter, local_scope, PCF_DBMS_RECURSE); + pcf_register_dbms_helper(mode, bufp, flag_parameter, local_scope, + PCF_DBMS_RECURSE); } #endif diff --git a/src/postconf/postconf_unused.c b/src/postconf/postconf_unused.c index d4416f8..717d1a5 100644 --- a/src/postconf/postconf_unused.c +++ b/src/postconf/postconf_unused.c @@ -2,7 +2,7 @@ /* NAME /* postconf_unused 3 /* SUMMARY -/* report unused parameters +/* report unused or deprecated parameters /* SYNOPSIS /* #include <postconf.h> /* @@ -15,11 +15,11 @@ /* In other words, don't call these functions with "postconf /* -d" which ignores user-defined main.cf settings. /* -/* pcf_flag_unused_main_parameters() reports unused "name=value" -/* entries in main.cf. +/* pcf_flag_unused_main_parameters() reports unused or deprecated +/* "name=value" entries in main.cf. /* -/* pcf_flag_unused_master_parameters() reports unused "-o -/* name=value" entries in master.cf. +/* pcf_flag_unused_master_parameters() reports unused or +/* deprecated "-o name=value" entries in master.cf. /* DIAGNOSTICS /* Problems are reported to the standard error stream. /* LICENSE @@ -31,6 +31,10 @@ /* IBM T.J. Watson Research /* P.O. Box 704 /* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* porcupine.org +/* Amawalk, NY 10501, USA /*--*/ /* System library. */ @@ -52,12 +56,66 @@ #include <postconf.h> + /* + * Deprecated parameter names and suggested alternatives. If we keep deleted + * parameter names in the table, a warning can still suggest alternatives. + * The downside of keeping deleted names in the table is that we may falsely + * warn about a user-defined parameter whose name matches that of a deleted + * parameter. + */ +typedef struct { + char *name; + char *alternative; +} PCF_DEPR_PARAM_INFO; + +static const PCF_DEPR_PARAM_INFO pcf_depr_param_info[] = { + + /* + * Parameters with deprecation warnings as of Postfix 3.9. The + * disable_dns_lookups parameter was documented as deprecated since + * Postfix 2.11 but nothing was logged. + */ + "disable_dns_lookups", "specify \"smtp_dns_support_level\"", + "lmtp_use_tls", "specify \"lmtp_tls_security_level\"", + "postscreen_use_tls", "specify \"postscreen_tls_security_level\"", + "smtp_use_tls", "specify \"smtp_tls_security_level\"", + "smtpd_use_tls", "specify \"smtpd_tls_security_level\"", + "tlsproxy_client_use_tls", "specify \"tlsproxy_client_security_level\"", + "tlsproxy_use_tls", "specify \"tlsproxy_tls_security_level\"", + "lmtp_enforce_tls", "lmtp_tls_security_level", + "postscreen_enforce_tls", "specify \"postscreen_tls_security_level\"", + "smtp_enforce_tls", "specify \"smtp_tls_security_level\"", + "smtpd_enforce_tls", "specify \"smtpd_tls_security_level\"", + "tlsproxy_client_enforce_tls", "specify \"tlsproxy_client_security_level\"", + "tlsproxy_enforce_tls", "specify \"tlsproxy_tls_security_level\"", + "lmtp_tls_per_site", "specify \"lmtp_tls_policy_maps\"", + "smtp_tls_per_site", "specify \"smtp_tls_policy_maps\"", + "tlsproxy_client_per_site", "specify \"tlsproxy_client_policy_maps\"", + "smtpd_tls_dh1024_param_file", "do not specify (leave at default)", + "smtpd_tls_eecdh_grade", "do not specify (leave at default)", + "deleted-test-only", "do not specify", /* For testing */ + 0, +}; +static HTABLE *pcf_depr_param_table; + +/* pcf_init_depr_params - initialize lookup table */ + +static void pcf_init_depr_params(void) +{ + const PCF_DEPR_PARAM_INFO *dp; + + pcf_depr_param_table = htable_create(30); + for (dp = pcf_depr_param_info; dp->name; dp++) + (void) htable_enter(pcf_depr_param_table, dp->name, (void *) dp); +} + /* pcf_flag_unused_parameters - warn about unused parameters */ static void pcf_flag_unused_parameters(DICT *dict, const char *conf_name, PCF_MASTER_ENT *local_scope) { const char *myname = "pcf_flag_unused_parameters"; + const PCF_DEPR_PARAM_INFO *dp; const char *param_name; const char *param_value; int how; @@ -67,23 +125,55 @@ static void pcf_flag_unused_parameters(DICT *dict, const char *conf_name, */ if (pcf_param_table == 0) msg_panic("%s: global parameter table is not initialized", myname); + if (dict->sequence == 0) + msg_panic("%s: parameter dictionary %s has no iterator", + myname, conf_name); + + /* + * One-time initialization. + */ + if (pcf_depr_param_table == 0) + pcf_init_depr_params(); /* * Iterate over all entries, and flag parameter names that aren't used - * anywhere. Show the warning message at the end of the output. + * anywhere, or that are deprecated. Show the warning message(s) after + * the end of the stdout output. */ - if (dict->sequence == 0) - msg_panic("%s: parameter dictionary %s has no iterator", - myname, conf_name); for (how = DICT_SEQ_FUN_FIRST; dict->sequence(dict, how, ¶m_name, ¶m_value) == 0; how = DICT_SEQ_FUN_NEXT) { + + /* + * Flag a parameter that is not used (deleted name, or incorrect + * name). + */ if (PCF_PARAM_TABLE_LOCATE(pcf_param_table, param_name) == 0 && (local_scope == 0 || PCF_PARAM_TABLE_LOCATE(local_scope->valid_names, param_name) == 0)) { vstream_fflush(VSTREAM_OUT); - msg_warn("%s/%s: unused parameter: %s=%s", - var_config_dir, conf_name, param_name, param_value); + if ((dp = (const PCF_DEPR_PARAM_INFO *) + htable_find(pcf_depr_param_table, param_name)) != 0) { + msg_warn("%s/%s: support for parameter %s has been removed;" + " instead, %s", var_config_dir, conf_name, + param_name, dp->alternative); + } else { + msg_warn("%s/%s: unused parameter: %s=%s", + var_config_dir, conf_name, param_name, param_value); + } + } + + /* + * Flag a parameter that is used but deprecated. Note that this may + * falsely complain about a user-defined parameter whose name matches + * that of a deleted parameter. + */ + else if ((dp = (const PCF_DEPR_PARAM_INFO *) + htable_find(pcf_depr_param_table, param_name)) != 0) { + vstream_fflush(VSTREAM_OUT); + msg_warn("%s/%s: support for parameter \"%s\" will be removed;" + " instead, %s", var_config_dir, conf_name, + param_name, dp->alternative); } } } diff --git a/src/postconf/postconf_user.c b/src/postconf/postconf_user.c index 5942ec0..46f4d61 100644 --- a/src/postconf/postconf_user.c +++ b/src/postconf/postconf_user.c @@ -6,7 +6,7 @@ /* SYNOPSIS /* #include <postconf.h> /* -/* void pcf_register_user_parameters() +/* void pcf_register_user_parameters(int mode) /* DESCRIPTION /* Postfix has multiple parameter name spaces: the global /* main.cf parameter name space, and the local parameter name @@ -40,6 +40,10 @@ /* to instantiate legacy per-dbms parameters, and to examine /* per-dbms configuration files. This is limited to the content /* of global and local, built-in and per-service, parameters. +/* +/* Arguments: +/* .IP mode +/* Passed on to pcf_register_dbms_parameters(). /* DIAGNOSTICS /* Problems are reported to the standard error stream. /* LICENSE @@ -224,7 +228,7 @@ static const char *pcf_lookup_eval(const char *dict_name, const char *name) /* pcf_scan_user_parameter_namespace - scan parameters in name space */ -static void pcf_scan_user_parameter_namespace(const char *dict_name, +static void pcf_scan_user_parameter_namespace(int mode, const char *dict_name, PCF_MASTER_ENT *local_scope) { const char *myname = "pcf_scan_user_parameter_namespace"; @@ -308,7 +312,7 @@ static void pcf_scan_user_parameter_namespace(const char *dict_name, */ if (node != 0 && (PCF_BUILTIN_PARAMETER(node) || PCF_SERVICE_PARAMETER(node))) - pcf_register_dbms_parameters(cparam_value, pcf_flag_user_parameter, + pcf_register_dbms_parameters(mode, cparam_value, pcf_flag_user_parameter, local_scope); #endif } @@ -345,7 +349,7 @@ static void pcf_scan_default_parameter_values(HTABLE *valid_params, /* pcf_register_user_parameters - add parameters with user-defined names */ -void pcf_register_user_parameters(void) +void pcf_register_user_parameters(int mode) { const char *myname = "pcf_register_user_parameters"; PCF_MASTER_ENT *masterp; @@ -403,7 +407,7 @@ void pcf_register_user_parameters(void) */ for (masterp = pcf_master_table; masterp->argv != 0; masterp++) if (masterp->all_params != 0) - pcf_scan_user_parameter_namespace(masterp->name_space, masterp); + pcf_scan_user_parameter_namespace(mode, masterp->name_space, masterp); /* * Scan parameter values that are left at their defaults in the global @@ -418,5 +422,5 @@ void pcf_register_user_parameters(void) /* * Scan the explicit name=value entries in the global name space. */ - pcf_scan_user_parameter_namespace(CONFIG_DICT, (PCF_MASTER_ENT *) 0); + pcf_scan_user_parameter_namespace(mode, CONFIG_DICT, (PCF_MASTER_ENT *) 0); } diff --git a/src/postconf/test29.ref b/src/postconf/test29.ref index 646890a..c3bbaec 100644 --- a/src/postconf/test29.ref +++ b/src/postconf/test29.ref @@ -2,15 +2,18 @@ config_directory = . ./postconf: warning: ./main.cf: unused parameter: pgsqlfoo_domain=bar ./postconf: warning: ./main.cf: unused parameter: sqlitefoo_domain=bar ./postconf: warning: ./main.cf: unused parameter: ldapxx=proxy:ldap:ldapfoo +./postconf: warning: ./main.cf: unused parameter: mongodbfoo_domain=bar ./postconf: warning: ./main.cf: unused parameter: sqlitexx=proxy:sqlite:sqlitefoo ./postconf: warning: ./main.cf: unused parameter: mysqlfoo_domain=bar ./postconf: warning: ./main.cf: unused parameter: sqlitefoo_domainx=bar ./postconf: warning: ./main.cf: unused parameter: memcachefoo_domain=bar ./postconf: warning: ./main.cf: unused parameter: pgsqlfoo_domainx=bar +./postconf: warning: ./main.cf: unused parameter: mongodbfoo_domainx=bar ./postconf: warning: ./main.cf: unused parameter: ldapfoo_domainx=bar ./postconf: warning: ./main.cf: unused parameter: ldapfoo_domain=bar ./postconf: warning: ./main.cf: unused parameter: memcachexx=proxy:memcache:memcachefoo ./postconf: warning: ./main.cf: unused parameter: memcachefoo_domainx=bar ./postconf: warning: ./main.cf: unused parameter: mysqlfoo_domainx=bar ./postconf: warning: ./main.cf: unused parameter: mysqlxx=proxy:mysql:mysqlfoo +./postconf: warning: ./main.cf: unused parameter: mongodbxx=proxy:mongodb:mongodbfoo ./postconf: warning: ./main.cf: unused parameter: pgsqlxx=proxy:pgsql:pgsqlfoo diff --git a/src/postconf/test72.ref b/src/postconf/test72.ref new file mode 100644 index 0000000..6b13cdc --- /dev/null +++ b/src/postconf/test72.ref @@ -0,0 +1,3 @@ +smtp unix - n n - 0 other +smtp fifo - n n - 0 other +smtp inet - n n - 0 other diff --git a/src/postconf/test73.ref b/src/postconf/test73.ref new file mode 100644 index 0000000..9554cc0 --- /dev/null +++ b/src/postconf/test73.ref @@ -0,0 +1,3 @@ +smtp unix - n n - 0 otherx +smtp fifo - n n - 0 other +smtp inet - n n - 0 other diff --git a/src/postconf/test74.ref b/src/postconf/test74.ref new file mode 100644 index 0000000..2886334 --- /dev/null +++ b/src/postconf/test74.ref @@ -0,0 +1,3 @@ +smtp unix - n n - 0 other +lmtp unix - n n - 0 otherx +smtp inet - n n - 0 other diff --git a/src/postconf/test75.ref b/src/postconf/test75.ref new file mode 100644 index 0000000..b8c54ab --- /dev/null +++ b/src/postconf/test75.ref @@ -0,0 +1,3 @@ +./postconf: warning: ignoring earlier request: 'mail_version = x' +config_directory = . +mail_version = y diff --git a/src/postconf/test76.ref b/src/postconf/test76.ref new file mode 100644 index 0000000..3e4cd26 --- /dev/null +++ b/src/postconf/test76.ref @@ -0,0 +1,9 @@ +config_directory = . +disable_dns_lookups = no +lmtp_use_tls = no +smtpd_tls_dh1024_param_file = auto +./postconf: warning: ./main.cf: support for parameter "disable_dns_lookups" will be removed; instead, specify "smtp_dns_support_level" +./postconf: warning: ./main.cf: support for parameter "lmtp_use_tls" will be removed; instead, specify "lmtp_tls_security_level" +./postconf: warning: ./main.cf: support for parameter "smtpd_tls_dh1024_param_file" will be removed; instead, do not specify (leave at default) +./postconf: warning: ./main.cf: support for parameter deleted-test-only has been removed; instead, do not specify +./postconf: warning: ./master.cf: support for parameter "smtp_enforce_tls" will be removed; instead, specify "smtp_tls_security_level" diff --git a/src/postdrop/postdrop.c b/src/postdrop/postdrop.c index e9335e9..66c9ea5 100644 --- a/src/postdrop/postdrop.c +++ b/src/postdrop/postdrop.c @@ -64,7 +64,7 @@ /* The default location of the Postfix main.cf and master.cf /* configuration files. /* .IP "\fBimport_environment (see 'postconf -d' output)\fR" -/* The list of environment parameters that a privileged Postfix +/* The list of environment variables that a privileged Postfix /* process will import from a non-Postfix parent process, or name=value /* environment overrides. /* .IP "\fBqueue_directory (see 'postconf -d' output)\fR" diff --git a/src/postfix/postfix.c b/src/postfix/postfix.c index c7f8b40..93bcecc 100644 --- a/src/postfix/postfix.c +++ b/src/postfix/postfix.c @@ -303,7 +303,7 @@ /* /* Table-driven mechanisms: /* access(5), Postfix SMTP access control table -/* aliases(5), Postfix alias database +/* aliases(5), Postfix local aliasing /* canonical(5), Postfix input address rewriting /* generic(5), Postfix output address rewriting /* header_checks(5), body_checks(5), Postfix content inspection @@ -316,6 +316,7 @@ /* ldap_table(5), Postfix LDAP client /* lmdb_table(5), Postfix LMDB database driver /* memcache_table(5), Postfix memcache client +/* mongodb_table(5), Postfix MongoDB client /* mysql_table(5), Postfix MYSQL client /* nisplus_table(5), Postfix NIS+ client /* pcre_table(5), Associate PCRE pattern with value diff --git a/src/postkick/postkick.c b/src/postkick/postkick.c index 6bf9245..a1ce55c 100644 --- a/src/postkick/postkick.c +++ b/src/postkick/postkick.c @@ -54,7 +54,7 @@ /* How long the \fBpostkick\fR(1) command waits for a request to enter the /* Postfix daemon process input buffer before giving up. /* .IP "\fBimport_environment (see 'postconf -d' output)\fR" -/* The list of environment parameters that a privileged Postfix +/* The list of environment variables that a privileged Postfix /* process will import from a non-Postfix parent process, or name=value /* environment overrides. /* .IP "\fBqueue_directory (see 'postconf -d' output)\fR" diff --git a/src/postlock/postlock.c b/src/postlock/postlock.c index a05d11e..972577b 100644 --- a/src/postlock/postlock.c +++ b/src/postlock/postlock.c @@ -79,7 +79,7 @@ /* The default location of the Postfix main.cf and master.cf /* configuration files. /* .IP "\fBimport_environment (see 'postconf -d' output)\fR" -/* The list of environment parameters that a privileged Postfix +/* The list of environment variables that a privileged Postfix /* process will import from a non-Postfix parent process, or name=value /* environment overrides. /* SEE ALSO diff --git a/src/postlog/postlog.c b/src/postlog/postlog.c index 7175f42..195ebd9 100644 --- a/src/postlog/postlog.c +++ b/src/postlog/postlog.c @@ -82,6 +82,12 @@ /* \fBpostlogd\fR(8) service. /* .IP "\fBpostlog_service_name (postlog)\fR" /* The name of the \fBpostlogd\fR(8) service entry in master.cf. +/* .PP +/* Available in Postfix 3.9 and later: +/* .IP "\fBmaillog_file_permissions (0600)\fR" +/* The file access permissions that will be set when the file +/* $maillog_file is created for the first time, or when the file is +/* created after an existing file is rotated. /* SEE ALSO /* postconf(5), configuration parameters /* postlogd(8), Postfix logging diff --git a/src/postlogd/postlogd.c b/src/postlogd/postlogd.c index 902cbe5..13f996f 100644 --- a/src/postlogd/postlogd.c +++ b/src/postlogd/postlogd.c @@ -31,10 +31,10 @@ /* CONFIGURATION PARAMETERS /* .ad /* .fi -/* Changes to \fBmain.cf\fR are picked up automatically, as -/* \fBpostlogd\fR(8) processes run for only a limited amount -/* of time. Use the command "\fBpostfix reload\fR" to speed -/* up a change. +/* Changes to \fBmain.cf\fR are not picked up automatically, +/* because \fBpostlogd\fR(8) terminates only after reaching +/* the \fBmax_idle\fR time limit. +/* Use the command "\fBpostfix reload\fR" to speed up a change. /* /* The text below provides only a parameter summary. See /* \fBpostconf\fR(5) for more details including examples. @@ -56,6 +56,12 @@ /* .IP "\fBpostlogd_watchdog_timeout (10s)\fR" /* How much time a \fBpostlogd\fR(8) process may take to process a request /* before it is terminated by a built-in watchdog timer. +/* .PP +/* Available in Postfix 3.9 and later: +/* .IP "\fBmaillog_file_permissions (0600)\fR" +/* The file access permissions that will be set when the file +/* $maillog_file is created for the first time, or when the file is +/* created after an existing file is rotated. /* SEE ALSO /* postconf(5), configuration parameters /* syslogd(8), system logging diff --git a/src/postmap/postmap.c b/src/postmap/postmap.c index 2826fc6..7390450 100644 --- a/src/postmap/postmap.c +++ b/src/postmap/postmap.c @@ -489,7 +489,7 @@ static void postmap(char *map_type, char *path_name, int postmap_flags, */ if ((mkmap->dict->flags & DICT_FLAG_UTF8_ACTIVE) && !allascii(STR(line_buffer)) - && !valid_utf8_string(STR(line_buffer), LEN(line_buffer))) { + && !valid_utf8_stringz(STR(line_buffer))) { msg_warn("%s, line %d: non-UTF-8 input \"%s\"" " -- ignoring this line", VSTREAM_PATH(source_fp), lineno, STR(line_buffer)); diff --git a/src/postqueue/showq_json.c b/src/postqueue/showq_json.c index db79404..a2820dd 100644 --- a/src/postqueue/showq_json.c +++ b/src/postqueue/showq_json.c @@ -58,69 +58,6 @@ #define STR(x) vstring_str(x) #define LEN(x) VSTRING_LEN(x) -/* json_quote - quote JSON string */ - -static char *json_quote(VSTRING *result, const char *text) -{ - unsigned char *cp; - int ch; - - /* - * We use short escape sequences for common control characters. Note that - * RFC 4627 allows "/" (0x2F) to be sent without quoting. Differences - * with RFC 4627: we send DEL (0x7f) as \u007F; the result remains RFC - * 4627 complaint. - */ - VSTRING_RESET(result); - for (cp = (unsigned char *) text; (ch = *cp) != 0; cp++) { - if (UNEXPECTED(ISCNTRL(ch))) { - switch (ch) { - case '\b': - VSTRING_ADDCH(result, '\\'); - VSTRING_ADDCH(result, 'b'); - break; - case '\f': - VSTRING_ADDCH(result, '\\'); - VSTRING_ADDCH(result, 'f'); - break; - case '\n': - VSTRING_ADDCH(result, '\\'); - VSTRING_ADDCH(result, 'n'); - break; - case '\r': - VSTRING_ADDCH(result, '\\'); - VSTRING_ADDCH(result, 'r'); - break; - case '\t': - VSTRING_ADDCH(result, '\\'); - VSTRING_ADDCH(result, 't'); - break; - default: - vstring_sprintf_append(result, "\\u%04X", ch); - break; - } - } else { - switch (ch) { - case '\\': - case '"': - VSTRING_ADDCH(result, '\\'); - /* FALLTHROUGH */ - default: - VSTRING_ADDCH(result, ch); - break; - } - } - } - VSTRING_TERMINATE(result); - - /* - * Force the result to be UTF-8 (with SMTPUTF8 enabled) or ASCII (with - * SMTPUTF8 disabled). - */ - printable(STR(result), '?'); - return (STR(result)); -} - /* json_message - report status for one message */ static void format_json(VSTREAM *showq_stream) @@ -148,6 +85,12 @@ static void format_json(VSTREAM *showq_stream) } /* + * Force JSON values to UTF-8 (with SMTPUTF8 enabled) or ASCII (with + * SMTPUTF8 disabled). + */ +#define QUOTE_JSON(res, src) printable(quote_for_json((res), (src), -1), '?') + + /* * Read the message properties and sender address. */ if (attr_scan(showq_stream, ATTR_FLAG_MORE | ATTR_FLAG_STRICT @@ -162,14 +105,14 @@ static void format_json(VSTREAM *showq_stream) msg_fatal_status(EX_SOFTWARE, "malformed showq server response"); vstream_printf("{"); vstream_printf("\"queue_name\": \"%s\", ", - json_quote(quote_buf, STR(queue_name))); + QUOTE_JSON(quote_buf, STR(queue_name))); vstream_printf("\"queue_id\": \"%s\", ", - json_quote(quote_buf, STR(queue_id))); + QUOTE_JSON(quote_buf, STR(queue_id))); vstream_printf("\"arrival_time\": %ld, ", arrival_time); vstream_printf("\"message_size\": %ld, ", message_size); vstream_printf("\"forced_expire\": %s, ", forced_expire ? "true" : "false"); vstream_printf("\"sender\": \"%s\", ", - json_quote(quote_buf, STR(addr))); + QUOTE_JSON(quote_buf, STR(addr))); /* * Read zero or more (recipient, reason) pair(s) until attr_scan_more() @@ -188,10 +131,10 @@ static void format_json(VSTREAM *showq_stream) ATTR_TYPE_END) != 2) msg_fatal_status(EX_SOFTWARE, "malformed showq server response"); vstream_printf("\"address\": \"%s\"", - json_quote(quote_buf, STR(addr))); + QUOTE_JSON(quote_buf, STR(addr))); if (LEN(why) > 0) vstream_printf(", \"delay_reason\": \"%s\"", - json_quote(quote_buf, STR(why))); + QUOTE_JSON(quote_buf, STR(why))); vstream_printf("}"); } vstream_printf("]"); diff --git a/src/postscreen/postscreen.c b/src/postscreen/postscreen.c index 192c2e9..ebb680c 100644 --- a/src/postscreen/postscreen.c +++ b/src/postscreen/postscreen.c @@ -291,25 +291,29 @@ /* The amount of time that \fBpostscreen\fR(8) will cache an expired /* temporary allowlist entry before it is removed. /* .IP "\fBpostscreen_bare_newline_ttl (30d)\fR" -/* The amount of time that \fBpostscreen\fR(8) will use the result from -/* a successful "bare newline" SMTP protocol test. +/* The amount of time that \fBpostscreen\fR(8) remembers that a client +/* IP address passed a "bare newline" SMTP protocol test, before it +/* address is required to pass that test again. /* .IP "\fBpostscreen_dnsbl_max_ttl (${postscreen_dnsbl_ttl?{$postscreen_dnsbl_ttl}:{1}}h)\fR" -/* The maximum amount of time that \fBpostscreen\fR(8) will use the -/* result from a successful DNS-based reputation test before a -/* client IP address is required to pass that test again. +/* The maximum amount of time that \fBpostscreen\fR(8) remembers that a +/* client IP address passed a DNS-based reputation test, before it is +/* required to pass that test again. /* .IP "\fBpostscreen_dnsbl_min_ttl (60s)\fR" -/* The minimum amount of time that \fBpostscreen\fR(8) will use the -/* result from a successful DNS-based reputation test before a -/* client IP address is required to pass that test again. +/* The minimum amount of time that \fBpostscreen\fR(8) remembers that a +/* client IP address passed a DNS-based reputation test, before it +/* is required to pass that test again. /* .IP "\fBpostscreen_greet_ttl (1d)\fR" -/* The amount of time that \fBpostscreen\fR(8) will use the result from -/* a successful PREGREET test. +/* The amount of time that \fBpostscreen\fR(8) remembers that a client +/* IP address passed a PREGREET test, before it is required to pass +/* that test again. /* .IP "\fBpostscreen_non_smtp_command_ttl (30d)\fR" -/* The amount of time that \fBpostscreen\fR(8) will use the result from -/* a successful "non_smtp_command" SMTP protocol test. +/* The amount of time that \fBpostscreen\fR(8) remembers that a client +/* IP address passed a "non_smtp_command" SMTP protocol test, before +/* it is required to pass that test again. /* .IP "\fBpostscreen_pipelining_ttl (30d)\fR" -/* The amount of time that \fBpostscreen\fR(8) will use the result from -/* a successful "pipelining" SMTP protocol test. +/* The amount of time that \fBpostscreen\fR(8) remembers that a client +/* IP address passed a "pipelining" SMTP protocol test, before it is +/* required to pass that test again. /* RESOURCE CONTROLS /* .ad /* .fi diff --git a/src/postscreen/postscreen_smtpd.c b/src/postscreen/postscreen_smtpd.c index dfc5d54..6b72626 100644 --- a/src/postscreen/postscreen_smtpd.c +++ b/src/postscreen/postscreen_smtpd.c @@ -874,7 +874,8 @@ static void psc_smtpd_read_event(int event, void *context) } /* - * Bare newline test. + * Bare newline test. Note: at this point, state->cmd_buffer is + * not null-terminated and may contain embedded null bytes. */ if (ch == '\n') { if ((state->flags & PSC_STATE_MASK_BARLF_TODO_SKIP) @@ -929,18 +930,19 @@ 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, len) \ - (var_smtputf8_enable && !valid_utf8_string((str), (len))) +#define PSC_BAD_UTF8(str) \ + (var_smtputf8_enable && !valid_utf8_stringz(str)) /* * Terminate the command buffer, and apply the last-resort command * editing workaround. */ VSTRING_TERMINATE(state->cmd_buffer); - if (psc_cmd_filter != 0 && !PSC_BAD_UTF8(STR(state->cmd_buffer), - LEN(state->cmd_buffer))) { + if (psc_cmd_filter != 0 && !PSC_BAD_UTF8(STR(state->cmd_buffer))) { const char *cp; for (cp = STR(state->cmd_buffer); *cp && IS_SPACE_TAB(*cp); cp++) @@ -1007,7 +1009,7 @@ static void psc_smtpd_read_event(int event, void *context) if ((state->flags & PSC_STATE_MASK_NSMTP_TODO_SKIP) == PSC_STATE_FLAG_NSMTP_TODO && cmdp->name == 0 && (is_header(command) - || PSC_BAD_UTF8(command, strlen(command)) + || PSC_BAD_UTF8(command) /* Ignore forbid_cmds lookup errors. Non-critical feature. */ || (*var_psc_forbid_cmds && string_list_match(psc_forbid_cmds, command)))) { diff --git a/src/postsuper/postsuper.c b/src/postsuper/postsuper.c index d3f2d5b..c9b8e38 100644 --- a/src/postsuper/postsuper.c +++ b/src/postsuper/postsuper.c @@ -288,7 +288,7 @@ /* The names of queue directories that are split across multiple /* subdirectory levels. /* .IP "\fBimport_environment (see 'postconf -d' output)\fR" -/* The list of environment parameters that a privileged Postfix +/* The list of environment variables that a privileged Postfix /* process will import from a non-Postfix parent process, or name=value /* environment overrides. /* .IP "\fBqueue_directory (see 'postconf -d' output)\fR" diff --git a/src/posttls-finger/posttls-finger.c b/src/posttls-finger/posttls-finger.c index d64c355..b474a40 100644 --- a/src/posttls-finger/posttls-finger.c +++ b/src/posttls-finger/posttls-finger.c @@ -103,7 +103,7 @@ /* in the DNS). In Postfix versions prior to 3.6, the default value /* was "md5". /* .IP "\fB-f\fR" -/* Lookup the associated DANE TLSA RRset even when a hostname is not an +/* Look up the associated DANE TLSA RRset even when a hostname is not an /* alias and its address records lie in an unsigned zone. See /* smtp_tls_force_insecure_host_tlsa_lookup for details. /* .IP "\fB-F \fICAfile.pem\fR (default: none)" @@ -264,6 +264,15 @@ /* the SMTP-in-SSL protocol, rather than the STARTTLS protocol. /* The destination \fIdomain\fR:\fIport\fR must of course provide such /* a service. +/* .IP "\fB-x\fR" +/* Prefer RFC7250 non-X.509 raw public key (RPK) server credentials. By +/* default only X.509 certificates are accepted. This is analogous to +/* setting \fBsmtp_tls_enable_rpk = yes\fR in the smtp(8) client. At the +/* fingerprint security level, when raw public keys are enabled, only +/* public key (and not certificate) fingerprints will be compared against +/* the specified list of \fImatch\fR arguments. Certificate fingerprints +/* are fragile when raw public keys are solicited, the server may at some +/* point in time start returning only the public key. /* .IP "\fB-X\fR" /* Enable \fBtlsproxy\fR(8) mode. This is an unsupported mode, /* for program development only. @@ -441,6 +450,9 @@ typedef struct OPTIONS { ARGV *tas; char *host_lookup; char *addr_pref; +#ifdef USE_TLS + int enable_rpk; +#endif } OPTIONS; /* @@ -696,7 +708,7 @@ static void print_stack(STATE *state, x509_stack_t *sk, int trustout) BIO_printf(state->tls_bio, " cert digest=%s\n", digest); myfree(digest); - digest = tls_pkey_fprint(cert, state->mdalg); + digest = tls_pkey_fprint(X509_get0_pubkey(cert), state->mdalg); BIO_printf(state->tls_bio, " pkey digest=%s\n", digest); myfree(digest); @@ -809,6 +821,7 @@ static int starttls(STATE *state) mdalg = state->mdalg); TLS_PROXY_CLIENT_START_PROPS(&start_props, timeout = smtp_tmout, + enable_rpk = state->options.enable_rpk, tls_level = state->level, nexthop = state->nexthop, host = state->hostname, @@ -826,7 +839,7 @@ static int starttls(STATE *state) state->ddane : state->dane); #define PROXY_OPEN_FLAGS \ - (TLS_PROXY_FLAG_ROLE_CLIENT | TLS_PROXY_FLAG_SEND_CONTEXT) + (TLS_PROXY_FLAG_ROLE_CLIENT | TLS_PROXY_FLAG_SEND_CONTEXT) #define var_tlsproxy_service if ((cwd_fd = open(".", O_RDONLY)) < 0) @@ -886,13 +899,19 @@ static int starttls(STATE *state) state->tls_context = tls_proxy_context_receive(state->stream); if (state->tls_context) { if (state->log_mask & - (TLS_LOG_CERTMATCH | TLS_LOG_VERBOSE | TLS_LOG_PEERCERT)) - msg_info("%s: subject_CN=%s, issuer_CN=%s, " - "fingerprint=%s, pkey_fingerprint=%s", - state->namaddrport, state->tls_context->peer_CN, - state->tls_context->issuer_CN, - state->tls_context->peer_cert_fprint, - state->tls_context->peer_pkey_fprint); + (TLS_LOG_CERTMATCH | TLS_LOG_VERBOSE | TLS_LOG_PEERCERT)) { + if (state->tls_context->stoc_rpk) + msg_info("%s: pkey_fingerprint=%s", state->namaddrport, + state->tls_context->peer_pkey_fprint); + else + msg_info("%s: subject_CN=%s, issuer_CN=%s, " + "fingerprint=%s, pkey_fingerprint=%s", + state->namaddrport, + state->tls_context->peer_CN, + state->tls_context->issuer_CN, + state->tls_context->peer_cert_fprint, + state->tls_context->peer_pkey_fprint); + } tls_log_summary(TLS_ROLE_CLIENT, TLS_USAGE_NEW, state->tls_context); } else { @@ -906,6 +925,7 @@ static int starttls(STATE *state) stream = stream, fd = -1, timeout = smtp_tmout, + enable_rpk = state->options.enable_rpk, tls_level = state->level, nexthop = state->nexthop, host = state->hostname, @@ -938,7 +958,7 @@ static int starttls(STATE *state) if (state->pass == 1) { ehlo(state); - if (!TLS_CERT_IS_PRESENT(state->tls_context)) + if (!TLS_CRED_IS_PRESENT(state->tls_context)) msg_info("Server is anonymous"); else if (state->tlsproxy_mode == 0) { if (state->print_trust) @@ -1232,7 +1252,7 @@ static DNS_RR *addr_one(STATE *state, DNS_RR *addr_list, const char *host, * should not clobber a soft error text and status code. */ #define RETRY_AI_ERROR(e) \ - ((e) == EAI_AGAIN || (e) == EAI_MEMORY || (e) == EAI_SYSTEM) + ((e) == EAI_AGAIN || (e) == EAI_MEMORY || (e) == EAI_SYSTEM) #ifdef EAI_NODATA #define DSN_NOHOST(e) \ ((e) == EAI_AGAIN || (e) == EAI_NODATA || (e) == EAI_NONAME) @@ -1833,14 +1853,14 @@ static void usage(void) #ifdef USE_TLS fprintf(stderr, "usage: %s %s \\\n\t%s \\\n\t%s \\\n\t%s \\\n\t%s" " destination [match ...]\n", var_procname, - "[-acCfSvw] [-t conn_tmout] [-T cmd_tmout] [-L logopts]", + "[-acCfRSvwx] [-t conn_tmout] [-T cmd_tmout] [-L logopts]", "[-h host_lookup] [-l level] [-d mdalg] [-g grade] [-p protocols]", "[-A tafile] [-F CAfile.pem] [-P CApath/] [-s servername]", "[ [-H chainfiles] | [-k certfile [-K keyfile]] ]", "[-m count] [-r delay] [-o name=value]"); #else - fprintf(stderr, "usage: %s [-acStTv] [-h host_lookup] [-o name=value] destination\n", - var_procname); + fprintf(stderr, "usage: %s [-acRStTv] [-h host_lookup] [-o name=value]" + " destination\n", var_procname); #endif exit(1); } @@ -1910,7 +1930,7 @@ static void parse_options(STATE *state, int argc, char *argv[]) #define OPTS "a:ch:o:RSt:T:v" #ifdef USE_TLS -#define TLSOPTS "A:Cd:fF:g:H:k:K:l:L:m:M:p:P:r:s:wX" +#define TLSOPTS "A:Cd:fF:g:H:k:K:l:L:m:M:p:P:r:s:wxX" state->mdalg = 0; state->CApath = mystrdup(""); @@ -1921,6 +1941,7 @@ static void parse_options(STATE *state, int argc, char *argv[]) state->sni = mystrdup(""); state->options.tas = argv_alloc(1); state->options.logopts = 0; + state->options.enable_rpk = 0; state->level = TLS_LEV_DANE; state->mxinsec_level = TLS_LEV_DANE; state->tlsproxy_mode = 0; @@ -2054,6 +2075,9 @@ static void parse_options(STATE *state, int argc, char *argv[]) case 'w': state->wrapper_mode = 1; break; + case 'x': + state->options.enable_rpk = 1; + break; case 'X': state->tlsproxy_mode = 1; break; @@ -2119,8 +2143,8 @@ static void parse_match(STATE *state, int argc, char *argv[]) int smtp_mode = 1; /* - * DANE match names are configured late, once the TLSA records are in - * hand. For now, prepare to fall back to "secure". + * DANE match names are configured late, once the TLSA records are in hand. + * For now, prepare to fall back to "secure". */ switch (state->level) { default: @@ -2148,8 +2172,8 @@ static void parse_match(STATE *state, int argc, char *argv[]) case TLS_LEV_FPRINT: state->dane = tls_dane_alloc(); while (*argv) - tls_dane_add_fpt_digests((TLS_DANE *) state->dane, *argv++, "", - smtp_mode); + tls_dane_add_fpt_digests(state->dane, state->options.enable_rpk, + *argv++, "", smtp_mode); break; } #endif diff --git a/src/proxymap/proxymap.c b/src/proxymap/proxymap.c index abdcf3a..c0af411 100644 --- a/src/proxymap/proxymap.c +++ b/src/proxymap/proxymap.c @@ -37,7 +37,7 @@ /* .IP \(bu /* To provide single-updater functionality for lookup tables /* that do not reliably support multiple writers (i.e. all -/* file-based tables). +/* file-based tables that are not based on \fBlmdb\fR). /* .PP /* The \fBproxymap\fR(8) server implements the following requests: /* .IP "\fBopen\fR \fImaptype:mapname flags\fR" @@ -752,8 +752,10 @@ static void post_jail_init(char *service_name, char **unused_argv) if (strcmp(service_name, MAIL_SERVICE_PROXYWRITE) == 0) proxy_writer = 1; else if (strcmp(service_name, MAIL_SERVICE_PROXYMAP) != 0) - msg_fatal("service name must be one of %s or %s", - MAIL_SERVICE_PROXYMAP, MAIL_SERVICE_PROXYMAP); + msg_fatal("invalid service name: \"%s\" - " + "service name must be \"%s\" or \"%s\"", + service_name, MAIL_SERVICE_PROXYWRITE, + MAIL_SERVICE_PROXYMAP); /* * Pre-allocate buffers. @@ -841,6 +843,36 @@ int main(int argc, char **argv) */ MAIL_VERSION_STAMP_ALLOCATE; + /* + * XXX When invoked with the master.cf service name "proxywrite", the + * proxymap daemon will allow update requests. To update a table that is + * not multi-writer safe (for example, some versions of Berkeley DB), the + * "proxywrite" service should run as a single updater (i.e. a process + * limit of 1, which could be enforced below by requesting + * CA_MAIL_SERVER_SOLITARY). + * + * In the default master.cf file, the "proxywrite" service has a process + * limit of 1. Assuming that updates will be rare, this process limit + * will suffice. Latency-sensitive services such as postscreen must not + * use the proxywrite service (in fact, postscreen has a latency check + * built-in). + * + * Optimizing for multi-writer operation would suffer from all kinds of + * complexity that would make it hard to use: + * + * - The master daemon specifies the "proxywrite" service name with the -n + * command-line option. This information is not known here, before the + * multi_server_main() call. The multi_server_main() function could + * reveal process limit information to its call-back functions, and leave + * single-updater enforcement to its call-back functions. + * + * - If we really want multi-writer update support, the "proxywrite" service + * would have to parse the $proxy_write_maps value, and permit + * multi-writer operation only if all tables are multi-writer safe. That + * would require a new dict(3) method, to query each lookup table + * implementation if it is multi-writer safe, without instantiating a + * lookup table client. + */ multi_server_main(argc, argv, proxymap_service, CA_MAIL_SERVER_STR_TABLE(str_table), CA_MAIL_SERVER_POST_INIT(post_jail_init), diff --git a/src/qmgr/qmgr.c b/src/qmgr/qmgr.c index 9d90a6e..25168e4 100644 --- a/src/qmgr/qmgr.c +++ b/src/qmgr/qmgr.c @@ -206,7 +206,7 @@ /* parameter value, where \fItransport\fR is the master.cf name of /* the message delivery transport. /* .IP "\fBdefault_recipient_refill_delay (5s)\fR" -/* The default per-transport maximum delay between recipients refills. +/* The default per-transport maximum delay between refilling recipients. /* .IP "\fBtransport_recipient_refill_delay ($default_recipient_refill_delay)\fR" /* A transport-specific override for the default_recipient_refill_delay /* parameter value, where \fItransport\fR is the master.cf name of diff --git a/src/qmgr/qmgr_deliver.c b/src/qmgr/qmgr_deliver.c index 577bb98..6c880ce 100644 --- a/src/qmgr/qmgr_deliver.c +++ b/src/qmgr/qmgr_deliver.c @@ -160,7 +160,7 @@ static int qmgr_deliver_send_request(QMGR_ENTRY *entry, VSTREAM *stream) */ for (recipient = list.info; recipient < list.info + list.len; recipient++) if (var_smtputf8_enable && (addr = recipient->address)[0] - && !allascii(addr) && valid_utf8_string(addr, strlen(addr))) { + && !allascii(addr) && valid_utf8_stringz(addr)) { smtputf8 |= SMTPUTF8_FLAG_RECIPIENT; if (message->verp_delims) smtputf8 |= SMTPUTF8_FLAG_SENDER; @@ -339,7 +339,7 @@ static void qmgr_deliver_update(int unused_event, void *context) #define SUSPENDED "delivery temporarily suspended: " if (status == DELIVER_STAT_CRASH) - DSN_SIMPLE(&dsb->dsn, "4.3.0", "unknown mail transport error"); + (void) DSN_SIMPLE(&dsb->dsn, "4.3.0", "unknown mail transport error"); if (status == DELIVER_STAT_CRASH || status == DELIVER_STAT_DEFER) { message->flags |= DELIVER_STAT_DEFER; if (VSTRING_LEN(dsb->status)) { diff --git a/src/qmqpd/qmqpd.c b/src/qmqpd/qmqpd.c index d94d33d..138ac64 100644 --- a/src/qmqpd/qmqpd.c +++ b/src/qmqpd/qmqpd.c @@ -57,7 +57,7 @@ /* Preliminary SMTPUTF8 support is introduced with Postfix 3.0. /* .IP "\fBsmtputf8_enable (yes)\fR" /* Enable preliminary SMTPUTF8 support for the protocols described -/* in RFC 6531..6533. +/* in RFC 6531, RFC 6532, and RFC 6533. /* .IP "\fBsmtputf8_autodetect_classes (sendmail, verify)\fR" /* Detect that a message requires SMTPUTF8 support for the specified /* mail origin classes. diff --git a/src/sendmail/sendmail.c b/src/sendmail/sendmail.c index 27b3543..df052c5 100644 --- a/src/sendmail/sendmail.c +++ b/src/sendmail/sendmail.c @@ -412,9 +412,10 @@ /* .IP "\fBqueue_directory (see 'postconf -d' output)\fR" /* The location of the Postfix top-level queue directory. /* .IP "\fBremote_header_rewrite_domain (empty)\fR" -/* Don't rewrite message headers from remote clients at all when -/* this parameter is empty; otherwise, rewrite message headers and -/* append the specified domain name to incomplete addresses. +/* Rewrite or add message headers in mail from remote clients if +/* the remote_header_rewrite_domain parameter value is non-empty, +/* updating incomplete addresses with the domain specified in the +/* remote_header_rewrite_domain parameter, and adding missing headers. /* .IP "\fBsyslog_facility (mail)\fR" /* The syslog facility of Postfix logging. /* .IP "\fBsyslog_name (see 'postconf -d' output)\fR" diff --git a/src/smtp/lmtp_params.c b/src/smtp/lmtp_params.c index bca7cd4..385c81f 100644 --- a/src/smtp/lmtp_params.c +++ b/src/smtp/lmtp_params.c @@ -4,6 +4,7 @@ VAR_BESTMX_TRANSP, DEF_BESTMX_TRANSP, &var_bestmx_transp, 0, 0, VAR_ERROR_RCPT, DEF_ERROR_RCPT, &var_error_rcpt, 1, 0, VAR_LMTP_SASL_PASSWD, DEF_LMTP_SASL_PASSWD, &var_smtp_sasl_passwd, 0, 0, + VAR_LMTP_SASL_PASSWD_RES_DELIM, DEF_LMTP_SASL_PASSWD_RES_DELIM, &var_smtp_sasl_passwd_res_delim, 1, 1, VAR_LMTP_SASL_OPTS, DEF_LMTP_SASL_OPTS, &var_smtp_sasl_opts, 0, 0, VAR_LMTP_SASL_PATH, DEF_LMTP_SASL_PATH, &var_smtp_sasl_path, 0, 0, #ifdef USE_TLS @@ -120,6 +121,7 @@ VAR_LMTP_TLS_NOTEOFFER, DEF_LMTP_TLS_NOTEOFFER, &var_smtp_tls_note_starttls_offer, VAR_LMTP_TLS_BLK_EARLY_MAIL_REPLY, DEF_LMTP_TLS_BLK_EARLY_MAIL_REPLY, &var_smtp_tls_blk_early_mail_reply, VAR_LMTP_TLS_FORCE_TLSA, DEF_LMTP_TLS_FORCE_TLSA, &var_smtp_tls_force_tlsa, + VAR_LMTP_TLS_ENABLE_RPK, DEF_LMTP_TLS_ENABLE_RPK, &var_smtp_tls_enable_rpk, #endif VAR_LMTP_TLS_WRAPPER, DEF_LMTP_TLS_WRAPPER, &var_smtp_tls_wrappermode, VAR_LMTP_SENDER_AUTH, DEF_LMTP_SENDER_AUTH, &var_smtp_sender_auth, diff --git a/src/smtp/smtp.c b/src/smtp/smtp.c index f7f2fc1..51b2e6d 100644 --- a/src/smtp/smtp.c +++ b/src/smtp/smtp.c @@ -1,17 +1,21 @@ /*++ /* NAME -/* smtp 8 +/* smtp, lmtp 8 /* SUMMARY /* Postfix SMTP+LMTP client /* SYNOPSIS /* \fBsmtp\fR [generic Postfix daemon options] [flags=DORX] +/* +/* \fBlmtp\fR [generic Postfix daemon options] [flags=DORX] /* DESCRIPTION /* The Postfix SMTP+LMTP client implements the SMTP and LMTP mail /* delivery protocols. It processes message delivery requests from /* the queue manager. Each request specifies a queue file, a sender /* address, a domain or host to deliver to, and recipient information. /* This program expects to be run from the \fBmaster\fR(8) process -/* manager. +/* manager. The process name, \fBsmtp\fR or \fBlmtp\fR, controls +/* the protocol, and the names of the configuration parameters +/* that will be used. /* /* The SMTP+LMTP client updates the queue file and marks recipients /* as finished, or it informs the queue manager that delivery should @@ -19,13 +23,9 @@ /* to the \fBbounce\fR(8), \fBdefer\fR(8) or \fBtrace\fR(8) daemon as /* appropriate. /* -/* The SMTP+LMTP client looks up a list of mail exchanger addresses for -/* the destination host, sorts the list by preference, and connects -/* to each listed address until it finds a server that responds. -/* -/* When a server is not reachable, or when mail delivery fails due -/* to a recoverable error condition, the SMTP+LMTP client will try to -/* deliver the mail to an alternate host. +/* The server lookup strategy is different for SMTP and LMTP, +/* as described in the sections "SMTP SERVER LOOKUP" and "LMTP +/* SERVER LOOKUP". /* /* After a successful mail transaction, a connection may be saved /* to the \fBscache\fR(8) connection cache server, so that it @@ -35,44 +35,58 @@ /* destinations that have a high volume of mail in the active /* queue. Connection caching can be enabled permanently for /* specific destinations. -/* SMTP DESTINATION SYNTAX +/* SMTP SERVER LOOKUP /* .ad /* .fi -/* The Postfix SMTP+LMTP client supports multiple destinations +/* The Postfix SMTP client supports multiple destinations /* separated by comma or whitespace (Postfix 3.5 and later). +/* Each destination is tried in the specified order. +/* /* SMTP destinations have the following form: /* .IP \fIdomainname\fR -/* .IP \fIdomainname\fR:\fIport\fR +/* .IP \fIdomainname\fR:\fIservice\fR /* Look up the mail exchangers for the specified domain, and -/* connect to the specified port (default: \fBsmtp\fR). +/* connect to the specified service (default: \fBsmtp\fR). +/* Optionally, mail exchangers may be looked up with SRV queries +/* instead of MX; this requires that \fIservice\fR is given +/* in symbolic form. /* .IP [\fIhostname\fR] -/* .IP [\fIhostname\fR]:\fIport\fR -/* Look up the address(es) of the specified host, and connect to -/* the specified port (default: \fBsmtp\fR). +/* .IP [\fIhostname\fR]:\fIservice\fR +/* Look up the address(es) for the specified host, and connect to +/* the specified service (default: \fBsmtp\fR). /* .IP [\fIaddress\fR] -/* .IP [\fIaddress\fR]:\fIport\fR +/* .IP [\fIaddress\fR]:\fIservice\fR /* Connect to the host at the specified address, and connect -/* to the specified port (default: \fBsmtp\fR). An IPv6 address +/* to the specified service (default: \fBsmtp\fR). An IPv6 address /* must be formatted as [\fBipv6\fR:\fIaddress\fR]. -/* LMTP DESTINATION SYNTAX +/* LMTP SERVER LOOKUP /* .ad /* .fi -/* The Postfix SMTP+LMTP client supports multiple destinations +/* The Postfix LMTP client supports multiple destinations /* separated by comma or whitespace (Postfix 3.5 and later). +/* Each destination is tried in the specified order. +/* /* LMTP destinations have the following form: /* .IP \fBunix\fR:\fIpathname\fR /* Connect to the local UNIX-domain server that is bound to the specified /* \fIpathname\fR. If the process runs chrooted, an absolute pathname /* is interpreted relative to the Postfix queue directory. +/* .IP \fBinet\fR:\fIdomainname\fR +/* .IP \fBinet\fR:\fIdomainname\fR:\fIservice\fR +/* Look up the LMTP servers for the specified domain and service +/* (default: \fBlmtp\fR). +/* This form is supported when SRV lookups are enabled, and +/* requires that \fIservice\fR is in symbolic form. /* .IP \fBinet\fR:\fIhostname\fR -/* .IP \fBinet\fR:\fIhostname\fR:\fIport\fR +/* .IP \fBinet\fR:\fIhostname\fR:\fIservice\fR +/* Look up the address(es) for the specified host, and connect to +/* the specified service (default: \fBlmtp\fR). When SRV lookups +/* are enabled, use the form \fB[\fIhostname\fB]\fR to force +/* address lookups. /* .IP \fBinet\fR:[\fIaddress\fR] -/* .IP \fBinet\fR:[\fIaddress\fR]:\fIport\fR -/* Connect to the specified TCP port on the specified local or -/* remote host. If no port is specified, connect to the port defined as -/* \fBlmtp\fR in \fBservices\fR(4). -/* If no such service is found, the \fBlmtp_tcp_port\fR configuration -/* parameter (default value of 24) will be used. +/* .IP \fBinet\fR:[\fIaddress\fR]:\fIservice\fR +/* Connect to the specified local or remote host and service +/* (default: \fBlmtp\fR). /* An IPv6 address must be formatted as [\fBipv6\fR:\fIaddress\fR]. /* SINGLE-RECIPIENT DELIVERY /* .ad @@ -130,6 +144,8 @@ /* This feature is available as of Postfix 3.5. /* .RE /* SECURITY +/* .ad +/* .fi /* The SMTP+LMTP client is moderately security-sensitive. It /* talks to SMTP or LMTP servers and to DNS servers on the /* network. The SMTP+LMTP client can be run chrooted at fixed @@ -175,11 +191,10 @@ /* CONFIGURATION PARAMETERS /* .ad /* .fi -/* Before Postfix version 2.3, the LMTP client is a separate -/* program that implements only a subset of the functionality -/* available with SMTP: there is no support for TLS, and -/* connections are cached in-process, making it ineffective -/* when the client is used for multiple domains. +/* Postfix versions 2.3 and later implement the SMTP and LMTP +/* client with the same program, and choose the protocol and +/* configuration parameters based on the process name, \fBsmtp\fR +/* or \fBlmtp\fR. /* /* Most smtp_\fIxxx\fR configuration parameters have an /* lmtp_\fIxxx\fR "mirror" parameter for the equivalent LMTP @@ -432,6 +447,11 @@ /* .IP "\fBsmtp_send_dummy_mail_auth (no)\fR" /* Whether or not to append the "AUTH=<>" option to the MAIL /* FROM command in SASL-authenticated SMTP sessions. +/* .PP +/* Available in Postfix version 3.9 and later: +/* .IP "\fBsmtp_sasl_password_result_delimiter (:)\fR" +/* The delimiter between username and password in sasl_passwd_maps lookup +/* results. /* STARTTLS SUPPORT CONTROLS /* .ad /* .fi @@ -532,7 +552,7 @@ /* certificate fingerprints. /* .PP /* Available in Postfix version 2.6 and later: -/* .IP "\fBsmtp_tls_protocols (see postconf -d output)\fR" +/* .IP "\fBsmtp_tls_protocols (see 'postconf -d' output)\fR" /* TLS protocols that the Postfix SMTP client will use with /* opportunistic TLS encryption. /* .IP "\fBsmtp_tls_ciphers (medium)\fR" @@ -613,6 +633,11 @@ /* .IP "\fBtls_config_name (empty)\fR" /* The application name passed by Postfix to OpenSSL library /* initialization functions. +/* .PP +/* Available in Postfix version 3.9 and later: +/* .IP "\fBsmtp_tls_enable_rpk (no)\fR" +/* Request that remote SMTP servers send an RFC7250 raw public key +/* instead of an X.509 certificate. /* OBSOLETE STARTTLS CONTROLS /* .ad /* .fi @@ -799,9 +824,9 @@ /* .IP "\fBdisable_dns_lookups (no)\fR" /* Disable DNS lookups in the Postfix SMTP and LMTP clients. /* .IP "\fBinet_interfaces (all)\fR" -/* The local network interface addresses that this mail system receives -/* mail on. -/* .IP "\fBinet_protocols (see 'postconf -d output')\fR" +/* The local network interface addresses that this mail system +/* receives mail on. +/* .IP "\fBinet_protocols (see 'postconf -d' output)\fR" /* The Internet protocols Postfix will attempt to use when making /* or accepting connections. /* .IP "\fBipc_timeout (3600s)\fR" @@ -1020,6 +1045,7 @@ int var_smtp_never_ehlo; char *var_smtp_sasl_opts; char *var_smtp_sasl_path; char *var_smtp_sasl_passwd; +char *var_smtp_sasl_passwd_res_delim; bool var_smtp_sasl_enable; char *var_smtp_sasl_mechs; char *var_smtp_sasl_type; @@ -1090,6 +1116,7 @@ char *var_smtp_tls_sni; bool var_smtp_tls_blk_early_mail_reply; bool var_smtp_tls_force_tlsa; char *var_smtp_tls_insecure_mx_policy; +bool var_smtp_tls_enable_rpk; #endif @@ -1117,8 +1144,8 @@ bool var_smtp_balance_inet_proto; bool var_smtp_req_deadline; int var_smtp_min_data_rate; char *var_use_srv_lookup; -bool var_ign_srv_lookup_err; -bool var_allow_srv_fallback; +bool var_ign_srv_lookup_err; +bool var_allow_srv_fallback; /* Special handling of 535 AUTH errors. */ char *var_smtp_sasl_auth_cache_name; @@ -1126,7 +1153,7 @@ int var_smtp_sasl_auth_cache_time; bool var_smtp_sasl_auth_soft_bounce; char *var_hfrom_format; -bool var_smtp_bind_addr_enforce; +bool var_smtp_bind_addr_enforce; /* * Global variables. @@ -1459,6 +1486,19 @@ static void pre_init(char *unused_name, char **unused_argv) }; /* + * The process name, "smtp" or "lmtp", determines the configuration + * parameters to use, protocol, DSN server reply type, SASL service + * information lookup, and more. We peeked at the name in the main() + * function before logging was initialized. Here, we detect and report an + * invalid process name. + */ + if (strcmp(var_procname, MAIL_PROC_NAME_SMTP) != 0 + && strcmp(var_procname, MAIL_PROC_NAME_LMTP) != 0) + msg_fatal("unexpected process name \"%s\" - " + "specify \"%s\" or \"%s\"", var_procname, + MAIL_PROC_NAME_SMTP, MAIL_PROC_NAME_LMTP); + + /* * Turn on per-peer debugging. */ debug_peer_init(); @@ -1649,21 +1689,15 @@ int main(int argc, char **argv) MAIL_VERSION_STAMP_ALLOCATE; /* - * XXX At this point, var_procname etc. are not initialized. - * - * The process name, "smtp" or "lmtp", determines the protocol, the DSN - * server reply type, SASL service information lookup, and more. Prepare - * for the possibility there may be another personality. + * XXX The process name, "smtp" or "lmtp", determines what configuration + * parameter settings to use, and more. However, at this point, logging + * and var_procname are not initialized. Here, we peek at the process + * name to determine what configuration parameter settings to use. Later, + * we detect and report an invalid process name. */ sane_procname = sane_basename((VSTRING *) 0, argv[0]); - if (strcmp(sane_procname, "smtp") == 0) + if (strcmp(sane_procname, MAIL_PROC_NAME_SMTP) == 0) smtp_mode = 1; - else if (strcmp(sane_procname, "lmtp") == 0) - smtp_mode = 0; - else - /* TODO: logging is not initialized. */ - msg_fatal("unexpected process name \"%s\" - " - "specify \"smtp\" or \"lmtp\"", var_procname); /* * Initialize with the LMTP or SMTP parameter name space. diff --git a/src/smtp/smtp.h b/src/smtp/smtp.h index f8c8f58..60c68f8 100644 --- a/src/smtp/smtp.h +++ b/src/smtp/smtp.h @@ -107,6 +107,7 @@ typedef struct SMTP_TLS_POLICY { TLS_DANE *dane; /* DANE TLSA digests */ char *sni; /* Optional SNI name when not DANE */ int conn_reuse; /* enable connection reuse */ + int enable_rpk; /* Enable server->client RPK */ } SMTP_TLS_POLICY; /* @@ -142,6 +143,7 @@ extern void smtp_tls_policy_cache_flush(void); _tls_policy_init_tmp->dane = 0; \ _tls_policy_init_tmp->sni = 0; \ _tls_policy_init_tmp->conn_reuse = 0; \ + _tls_policy_init_tmp->enable_rpk = 0; \ } while (0) #endif diff --git a/src/smtp/smtp_addr.c b/src/smtp/smtp_addr.c index f30ce73..8c384fc 100644 --- a/src/smtp/smtp_addr.c +++ b/src/smtp/smtp_addr.c @@ -262,7 +262,7 @@ static DNS_RR *smtp_addr_one(DNS_RR *addr_list, const char *host, int res_opt, msg_fatal("host %s: conversion error for address family " "%d: %m", host, res0->ai_addr->sa_family); addr_list = dns_rr_append(addr_list, addr); - if (DNS_RR_IS_TRUNCATED(addr_list)) + if (DNS_RR_IS_TRUNCATED(addr_list)) break; if (msg_verbose) { MAI_HOSTADDR_STR hostaddr_str; diff --git a/src/smtp/smtp_params.c b/src/smtp/smtp_params.c index 22f4709..cebff93 100644 --- a/src/smtp/smtp_params.c +++ b/src/smtp/smtp_params.c @@ -4,6 +4,7 @@ VAR_BESTMX_TRANSP, DEF_BESTMX_TRANSP, &var_bestmx_transp, 0, 0, VAR_ERROR_RCPT, DEF_ERROR_RCPT, &var_error_rcpt, 1, 0, VAR_SMTP_SASL_PASSWD, DEF_SMTP_SASL_PASSWD, &var_smtp_sasl_passwd, 0, 0, + VAR_SMTP_SASL_PASSWD_RES_DELIM, DEF_SMTP_SASL_PASSWD_RES_DELIM, &var_smtp_sasl_passwd_res_delim, 1, 1, VAR_SMTP_SASL_OPTS, DEF_SMTP_SASL_OPTS, &var_smtp_sasl_opts, 0, 0, VAR_SMTP_SASL_PATH, DEF_SMTP_SASL_PATH, &var_smtp_sasl_path, 0, 0, #ifdef USE_TLS @@ -124,6 +125,7 @@ VAR_SMTP_TLS_NOTEOFFER, DEF_SMTP_TLS_NOTEOFFER, &var_smtp_tls_note_starttls_offer, VAR_SMTP_TLS_BLK_EARLY_MAIL_REPLY, DEF_SMTP_TLS_BLK_EARLY_MAIL_REPLY, &var_smtp_tls_blk_early_mail_reply, VAR_SMTP_TLS_FORCE_TLSA, DEF_SMTP_TLS_FORCE_TLSA, &var_smtp_tls_force_tlsa, + VAR_SMTP_TLS_ENABLE_RPK, DEF_SMTP_TLS_ENABLE_RPK, &var_smtp_tls_enable_rpk, #endif VAR_SMTP_TLS_WRAPPER, DEF_SMTP_TLS_WRAPPER, &var_smtp_tls_wrappermode, VAR_SMTP_SENDER_AUTH, DEF_SMTP_SENDER_AUTH, &var_smtp_sender_auth, diff --git a/src/smtp/smtp_proto.c b/src/smtp/smtp_proto.c index 097d518..e022bc2 100644 --- a/src/smtp/smtp_proto.c +++ b/src/smtp/smtp_proto.c @@ -929,6 +929,7 @@ static int smtp_start_tls(SMTP_STATE *state) TLS_PROXY_CLIENT_START_PROPS(&start_props, timeout = var_smtp_starttls_tmout, tls_level = state->tls->level, + enable_rpk = state->tls->enable_rpk, nexthop = session->tls_nexthop, host = STR(iter->host), namaddr = session->namaddrport, @@ -1051,6 +1052,7 @@ static int smtp_start_tls(SMTP_STATE *state) fd = -1, timeout = var_smtp_starttls_tmout, tls_level = state->tls->level, + enable_rpk = state->tls->enable_rpk, nexthop = session->tls_nexthop, host = STR(iter->host), namaddr = session->namaddrport, diff --git a/src/smtp/smtp_sasl_glue.c b/src/smtp/smtp_sasl_glue.c index ef8e8c4..cce5ef7 100644 --- a/src/smtp/smtp_sasl_glue.c +++ b/src/smtp/smtp_sasl_glue.c @@ -200,7 +200,9 @@ int smtp_sasl_passwd_lookup(SMTP_SESSION *session) if (session->sasl_username) myfree(session->sasl_username); session->sasl_username = mystrdup(value); - passwd = split_at(session->sasl_username, ':'); + /* Historically, the delimiter may appear in the password. */ + passwd = split_at(session->sasl_username, + *var_smtp_sasl_passwd_res_delim); if (session->sasl_passwd) myfree(session->sasl_passwd); session->sasl_passwd = mystrdup(passwd ? passwd : ""); diff --git a/src/smtp/smtp_tls_policy.c b/src/smtp/smtp_tls_policy.c index 92a231d..f407d65 100644 --- a/src/smtp/smtp_tls_policy.c +++ b/src/smtp/smtp_tls_policy.c @@ -334,9 +334,10 @@ static void tls_policy_lookup_one(SMTP_TLS_POLICY *tls, int *site_level, INVALID_RETURN(tls->why, site_level); break; case TLS_LEV_FPRINT: - if (!tls->dane) - tls->dane = tls_dane_alloc(); - tls_dane_add_fpt_digests(tls->dane, val, "|", smtp_mode); + if (tls->matchargv == 0) + tls->matchargv = argv_split(val, "|"); + else + argv_split_append(tls->matchargv, val, "|"); break; case TLS_LEV_VERIFY: case TLS_LEV_SECURE: @@ -390,6 +391,19 @@ static void tls_policy_lookup_one(SMTP_TLS_POLICY *tls, int *site_level, } continue; } + if (!strcasecmp(name, "enable_rpk")) { + /* Ultimately ignored at some security levels */ + if (strcasecmp(val, "yes") == 0) { + tls->enable_rpk = 1; + } else if (strcasecmp(val, "no") == 0) { + tls->enable_rpk = 0; + } else { + msg_warn("%s: attribute \"%s\" has bad value: \"%s\"", + WHERE, name, val); + INVALID_RETURN(tls->why, site_level); + } + continue; + } msg_warn("%s: invalid attribute name: \"%s\"", WHERE, name); INVALID_RETURN(tls->why, site_level); } @@ -518,6 +532,7 @@ static void *policy_create(const char *unused_key, void *context) smtp_tls_policy_init(tls, dsb_create()); tls->conn_reuse = var_smtp_tls_conn_reuse; + tls->enable_rpk = var_smtp_tls_enable_rpk; /* * Compute the per-site TLS enforcement level. For compatibility with the @@ -602,6 +617,13 @@ static void *policy_create(const char *unused_key, void *context) */ set_cipher_grade(tls); +/* + * Even when soliciting raw public keys, synthesize TLSA RRs that also match + * certificates. Though this is fragile, it maintains compatibility with + * servers that never return RPKs. + */ +#define DONT_SUPPRESS_CERT_MATCH 0 + /* * Use main.cf cert_match setting if not set in per-destination table. */ @@ -617,16 +639,26 @@ static void *policy_create(const char *unused_key, void *context) case TLS_LEV_FPRINT: if (tls->dane == 0) tls->dane = tls_dane_alloc(); - if (tls->dane->tlsa == 0) { - tls_dane_add_fpt_digests(tls->dane, var_smtp_tls_fpt_cmatch, - CHARS_COMMA_SP, smtp_mode); - if (tls->dane->tlsa == 0) { - msg_warn("nexthop domain %s: configured at fingerprint " - "security level, but with no fingerprints to match.", - dest); - MARK_INVALID(tls->why, &tls->level); - return ((void *) tls); + /* Process the specified fingerprint match patterns */ + if (tls->matchargv) { + int i; + + for (i = 0; i < tls->matchargv->argc; ++i) { + tls_dane_add_fpt_digests(tls->dane, DONT_SUPPRESS_CERT_MATCH, + tls->matchargv->argv[i], "", + smtp_mode); } + } else { + tls_dane_add_fpt_digests(tls->dane, DONT_SUPPRESS_CERT_MATCH, + var_smtp_tls_fpt_cmatch, CHARS_COMMA_SP, + smtp_mode); + } + if (tls->dane->tlsa == 0) { + msg_warn("nexthop domain %s: configured at fingerprint " + "security level, but with no fingerprints to match.", + dest); + MARK_INVALID(tls->why, &tls->level); + return ((void *) tls); } break; case TLS_LEV_VERIFY: diff --git a/src/smtpd/Makefile.in b/src/smtpd/Makefile.in index 7fdfe12..c8837fe 100644 --- a/src/smtpd/Makefile.in +++ b/src/smtpd/Makefile.in @@ -75,7 +75,8 @@ broken-tests: smtpd_check_test smtpd_check_test2 tests: smtpd_acl_test smtpd_addr_valid_test smtpd_exp_test \ smtpd_token_test smtpd_check_test4 smtpd_check_dsn_test \ smtpd_check_backup_test smtpd_dnswl_test smtpd_error_test \ - smtpd_server_test smtpd_nullmx_test smtpd_dns_filter_test + smtpd_server_test smtpd_nullmx_test smtpd_dns_filter_test \ + smtpd_deprecated_test root_tests: @@ -114,7 +115,8 @@ smtpd_addr_valid_test: smtpd_check smtpd_addr_valid.in smtpd_addr_valid.ref # This requires that the DNS server can query porcupine.org. -ADDRINFO_FIX = sed 's/No address associated with hostname/hostname nor servname provided, or not known/' +ADDRINFO_FIX = sed -e 's/No address associated with hostname/hostname nor servname provided, or not known/' \ + -e 's/Name or service not known/hostname nor servname provided, or not known/' smtpd_exp_test: smtpd_check smtpd_exp.in smtpd_exp.ref $(SHLIB_ENV) $(VALGRIND) ../postmap/postmap hash:smtpd_check_access @@ -170,6 +172,11 @@ smtpd_error_test: smtpd_check smtpd_error.in smtpd_error.ref diff smtpd_error.ref smtpd_check.tmp rm -f smtpd_check.tmp +smtpd_deprecated_test: smtpd_check smtpd_deprecated.in smtpd_deprecated.ref + $(SHLIB_ENV) $(VALGRIND) ./smtpd_check <smtpd_deprecated.in >smtpd_check.tmp 2>&1 + diff smtpd_deprecated.ref smtpd_check.tmp + rm -f smtpd_check.tmp + depend: $(MAKES) (sed '1,/^# do not edit/!d' Makefile.in; \ set -e; for i in [a-z][a-z0-9]*.c; do \ diff --git a/src/smtpd/smtpd.c b/src/smtpd/smtpd.c index 6a2cf01..bce0d43 100644 --- a/src/smtpd/smtpd.c +++ b/src/smtpd/smtpd.c @@ -468,7 +468,7 @@ /* \fBcheck_ccert_access\fR and \fBpermit_tls_clientcerts\fR. /* .PP /* Available in Postfix version 2.6 and later: -/* .IP "\fBsmtpd_tls_protocols (see postconf -d output)\fR" +/* .IP "\fBsmtpd_tls_protocols (see 'postconf -d' output)\fR" /* TLS protocols accepted by the Postfix SMTP server with opportunistic /* TLS encryption. /* .IP "\fBsmtpd_tls_ciphers (medium)\fR" @@ -537,6 +537,12 @@ /* .IP "\fBtls_config_name (empty)\fR" /* The application name passed by Postfix to OpenSSL library /* initialization functions. +/* .PP +/* Available in Postfix version 3.9 and later: +/* .IP "\fBsmtpd_tls_enable_rpk (no)\fR" +/* Request that remote SMTP clients send an RFC7250 raw public key +/* instead of an X.509 certificate, when asking for or requiring client +/* authentication. /* OBSOLETE STARTTLS CONTROLS /* .ad /* .fi @@ -662,12 +668,12 @@ /* The list of domains that are delivered via the $local_transport /* mail delivery transport. /* .IP "\fBinet_interfaces (all)\fR" -/* The local network interface addresses that this mail system receives -/* mail on. +/* The local network interface addresses that this mail system +/* receives mail on. /* .IP "\fBproxy_interfaces (empty)\fR" /* The remote network interface addresses that this mail system receives mail /* on by way of a proxy or network address translation unit. -/* .IP "\fBinet_protocols (see 'postconf -d output')\fR" +/* .IP "\fBinet_protocols (see 'postconf -d' output)\fR" /* The Internet protocols Postfix will attempt to use when making /* or accepting connections. /* .IP "\fBlocal_recipient_maps (proxy:unix:passwd.byname $alias_maps)\fR" @@ -698,8 +704,9 @@ /* alias domains, that is, domains for which all addresses are aliased /* to addresses in other local or remote domains. /* .IP "\fBvirtual_alias_maps ($virtual_maps)\fR" -/* Optional lookup tables that alias specific mail addresses or domains -/* to other local or remote addresses. +/* Optional lookup tables with aliases that apply to all recipients: +/* \fBlocal\fR(8), virtual, and remote; this is unlike alias_maps that apply +/* only to \fBlocal\fR(8) recipients. /* .IP "\fBunknown_virtual_alias_reject_code (550)\fR" /* The Postfix SMTP server reply code when a recipient address matches /* $virtual_alias_domains, and $virtual_alias_maps specifies a list @@ -817,7 +824,7 @@ /* command pipelining constraints. /* .PP /* Available in Postfix 3.9, 3.8.4, 3.7.9, 3.6.13, 3.5.23 and later: -/* .IP "\fBsmtpd_forbid_bare_newline (Postfix < 3.9: no)\fR" +/* .IP "\fBsmtpd_forbid_bare_newline (Postfix >= 3.9: normalize)\fR" /* Reject or restrict input lines from an SMTP client that end in /* <LF> instead of the standard <CR><LF>. /* .IP "\fBsmtpd_forbid_bare_newline_exclusions ($mynetworks)\fR" @@ -1492,6 +1499,7 @@ char *var_smtpd_tls_eecdh; char *var_smtpd_tls_eccert_file; char *var_smtpd_tls_eckey_file; char *var_smtpd_tls_chain_files; +int var_smtpd_tls_enable_rpk; #endif @@ -1664,13 +1672,16 @@ int smtpd_hfrom_format; */ #define BARE_LF_FLAG_WANT_STD_EOD (1<<0) /* Require CRLF.CRLF */ #define BARE_LF_FLAG_REPLY_REJECT (1<<1) /* Reject bare newline */ +#define BARE_LF_FLAG_NOTE_LOG (1<<2) /* Note bare newline */ #define IS_BARE_LF_WANT_STD_EOD(m) ((m) & BARE_LF_FLAG_WANT_STD_EOD) #define IS_BARE_LF_REPLY_REJECT(m) ((m) & BARE_LF_FLAG_REPLY_REJECT) +#define IS_BARE_LF_NOTE_LOG(m) ((m) & BARE_LF_FLAG_NOTE_LOG) static const NAME_CODE bare_lf_mask_table[] = { "normalize", BARE_LF_FLAG_WANT_STD_EOD, /* Default */ "yes", BARE_LF_FLAG_WANT_STD_EOD, /* Migration aid */ + "note", BARE_LF_FLAG_WANT_STD_EOD | BARE_LF_FLAG_NOTE_LOG, "reject", BARE_LF_FLAG_WANT_STD_EOD | BARE_LF_FLAG_REPLY_REJECT, "no", 0, 0, -1, /* error */ @@ -3504,11 +3515,15 @@ static void common_pre_message_handling(SMTPD_STATE *state, } if (state->tls_context->srvr_sig_curve && *state->tls_context->srvr_sig_curve) - vstring_sprintf_append(state->buffer, " (%s)", - state->tls_context->srvr_sig_curve); + vstring_sprintf_append(state->buffer, " (%s%s)", + state->tls_context->srvr_sig_curve, + state->tls_context->stoc_rpk ? + " raw public key" : ""); else if (state->tls_context->srvr_sig_bits > 0) - vstring_sprintf_append(state->buffer, " (%d bits)", - state->tls_context->srvr_sig_bits); + vstring_sprintf_append(state->buffer, " (%d bit%s)", + state->tls_context->srvr_sig_bits, + state->tls_context->stoc_rpk ? + " raw public key" : "s"); if (state->tls_context->srvr_sig_dgst && *state->tls_context->srvr_sig_dgst) vstring_sprintf_append(state->buffer, " server-digest %s", @@ -3522,11 +3537,15 @@ static void common_pre_message_handling(SMTPD_STATE *state, state->tls_context->clnt_sig_name); if (state->tls_context->clnt_sig_curve && *state->tls_context->clnt_sig_curve) - vstring_sprintf_append(state->buffer, " (%s)", - state->tls_context->clnt_sig_curve); + vstring_sprintf_append(state->buffer, " (%s%s)", + state->tls_context->clnt_sig_curve, + state->tls_context->ctos_rpk ? + " raw public key" : ""); else if (state->tls_context->clnt_sig_bits > 0) - vstring_sprintf_append(state->buffer, " (%d bits)", - state->tls_context->clnt_sig_bits); + vstring_sprintf_append(state->buffer, " (%d bit%s)", + state->tls_context->clnt_sig_bits, + state->tls_context->ctos_rpk ? + " raw public key" : "s"); if (state->tls_context->clnt_sig_dgst && *state->tls_context->clnt_sig_dgst) vstring_sprintf_append(state->buffer, " client-digest %s", @@ -3546,6 +3565,11 @@ static void common_pre_message_handling(SMTPD_STATE *state, "verified OK" : "not verified"); vstring_free(issuer_CN); vstring_free(peer_CN); + } else if (TLS_RPK_IS_PRESENT(state->tls_context)) { + out_fprintf(out_stream, REC_TYPE_NORM, + "\t(Client RPK %s digest %s)", + var_smtpd_tls_fpt_dgst, + state->tls_context->peer_pkey_fprint); } else if (var_smtpd_tls_ask_ccert) out_fprintf(out_stream, REC_TYPE_NORM, "\t(Client did not present a certificate)"); @@ -3648,6 +3672,8 @@ static void receive_data_message(SMTPD_STATE *state, curr_rec_type = REC_TYPE_CONT; if (IS_BARE_LF_REPLY_REJECT(smtp_got_bare_lf)) state->err |= CLEANUP_STAT_BARE_LF; + else if (IS_BARE_LF_NOTE_LOG(smtp_got_bare_lf)) + state->notes |= SMTPD_NOTE_BARE_LF; start = vstring_str(state->buffer); len = VSTRING_LEN(state->buffer); if (first) { @@ -4168,6 +4194,8 @@ static int bdat_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *argv) } if (IS_BARE_LF_REPLY_REJECT(smtp_got_bare_lf)) state->err |= CLEANUP_STAT_BARE_LF; + else if (IS_BARE_LF_NOTE_LOG(smtp_got_bare_lf)) + state->notes |= SMTPD_NOTE_BARE_LF; start = vstring_str(state->bdat_get_buffer); len = VSTRING_LEN(state->bdat_get_buffer); if (state->err == CLEANUP_STAT_OK) { @@ -5231,6 +5259,7 @@ static void smtpd_start_tls(SMTPD_STATE *state) stream = state->client, fd = -1, timeout = var_smtpd_starttls_tmout, + enable_rpk = var_smtpd_tls_enable_rpk, requirecert = requirecert, serverid = state->service, namaddr = state->namaddr, @@ -5469,8 +5498,6 @@ static void tls_reset(SMTPD_STATE *state) #endif -#if !defined(USE_TLS) || !defined(USE_SASL_AUTH) - /* unimpl_cmd - dummy for functionality that is not compiled in */ static int unimpl_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *unused_argv) @@ -5487,8 +5514,6 @@ static int unimpl_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *unused_argv) return (-1); } -#endif - /* * The table of all SMTP commands that we know. Set the junk limit flag on * any command that can be repeated an arbitrary number of times without @@ -5513,6 +5538,8 @@ typedef struct SMTPD_CMD { #define SMTPD_CMD_FLAG_PRE_TLS (1<<1) /* allow before STARTTLS */ #define SMTPD_CMD_FLAG_LAST (1<<2) /* last in PIPELINING command group */ +static int help_cmd(SMTPD_STATE *, int, SMTPD_TOKEN *); + static SMTPD_CMD smtpd_cmd_table[] = { {SMTPD_CMD_HELO, helo_cmd, SMTPD_CMD_FLAG_LIMIT | SMTPD_CMD_FLAG_PRE_TLS | SMTPD_CMD_FLAG_LAST,}, {SMTPD_CMD_EHLO, ehlo_cmd, SMTPD_CMD_FLAG_LIMIT | SMTPD_CMD_FLAG_PRE_TLS | SMTPD_CMD_FLAG_LAST,}, @@ -5537,12 +5564,44 @@ static SMTPD_CMD smtpd_cmd_table[] = { {SMTPD_CMD_VRFY, vrfy_cmd, SMTPD_CMD_FLAG_LIMIT | SMTPD_CMD_FLAG_LAST,}, {SMTPD_CMD_ETRN, etrn_cmd, SMTPD_CMD_FLAG_LIMIT,}, {SMTPD_CMD_QUIT, quit_cmd, SMTPD_CMD_FLAG_PRE_TLS,}, + {SMTPD_CMD_HELP, help_cmd, SMTPD_CMD_FLAG_PRE_TLS,}, {0,}, }; static STRING_LIST *smtpd_noop_cmds; static STRING_LIST *smtpd_forbid_cmds; +/* help_cmd - process HELP command */ + +static int help_cmd(SMTPD_STATE *state, int unused_argc, SMTPD_TOKEN *unused_argv) +{ + ARGV *argv = argv_alloc(sizeof(smtpd_cmd_table) + / sizeof(*smtpd_cmd_table)); + VSTRING *buf = vstring_alloc(100); + SMTPD_CMD *cmdp; + + /* + * Return a list of implemented commands. + * + * The HELP command does not suppress commands that can be dynamically + * disabled in the EHLO response or through access control. That would + * require refactoring the EHLO feature-suppression and per-feature + * access control, so that they can be reused (not duplicated) here. + * + * The HELP command does not provide information that makes Postfix easier + * to fingerprint, such as software name, version, or build information. + */ + for (cmdp = smtpd_cmd_table; cmdp->name != 0; cmdp++) + if (cmdp->action != unimpl_cmd) + argv_add(argv, cmdp->name, ARGV_END); + argv_sort(argv); + smtpd_chat_reply(state, "214 2.0.0 Commands: %s", + argv_join(buf, argv, ' ')); + vstring_free(buf); + argv_free(argv); + return (0); +} + /* smtpd_flag_ill_pipelining - flag pipelining protocol violation */ static int smtpd_flag_ill_pipelining(SMTPD_STATE *state) @@ -5869,9 +5928,11 @@ static void smtpd_proto(SMTPD_STATE *state) var_smtpd_forbid_bare_lf_code, var_myhostname); break; } + if (IS_BARE_LF_NOTE_LOG(smtp_got_bare_lf)) + state->notes |= SMTPD_NOTE_BARE_LF; /* Safety: protect internal interfaces against malformed UTF-8. */ - if (var_smtputf8_enable && valid_utf8_string(STR(state->buffer), - LEN(state->buffer)) == 0) { + if (var_smtputf8_enable + && valid_utf8_stringz(STR(state->buffer)) == 0) { state->error_mask |= MAIL_ERROR_PROTOCOL; smtpd_chat_reply(state, "500 5.5.2 Error: bad UTF-8 syntax"); state->error_count++; @@ -6055,11 +6116,12 @@ static void smtpd_proto(SMTPD_STATE *state) /* smtpd_format_cmd_stats - format per-command statistics */ -static char *smtpd_format_cmd_stats(VSTRING *buf) +static char *smtpd_format_cmd_stats(SMTPD_STATE *state) { SMTPD_CMD *cmdp; int all_success = 0; int all_total = 0; + VSTRING *buf = state->buffer; /* * Log the statistics. Note that this loop produces no output when no @@ -6103,6 +6165,13 @@ static char *smtpd_format_cmd_stats(VSTRING *buf) vstring_sprintf_append(buf, " commands=%d", all_success); if (all_success != all_total || all_total == 0) vstring_sprintf_append(buf, "/%d", all_total); + + /* + * Log aggregated warnings. + */ + if (state->notes & SMTPD_NOTE_BARE_LF) + vstring_sprintf_append(buf, " notes=bare_lf"); + return (lowercase(STR(buf))); } @@ -6244,7 +6313,7 @@ static void smtpd_service(VSTREAM *stream, char *service, char **argv) * connection time. */ msg_info("disconnect from %s%s", state.namaddr, - smtpd_format_cmd_stats(state.buffer)); + smtpd_format_cmd_stats(&state)); teardown_milters(&state); /* duplicates xclient_cmd */ smtpd_state_reset(&state); debug_peer_restore(); @@ -6403,7 +6472,6 @@ static void pre_jail_init(char *unused_name, char **unused_argv) no_server_cert_ok = 0; cert_file = var_smtpd_tls_cert_file; } - have_server_cert = *cert_file != 0; have_server_cert |= *var_smtpd_tls_eccert_file != 0; have_server_cert |= *var_smtpd_tls_dcert_file != 0; @@ -6653,6 +6721,7 @@ int main(int argc, char **argv) #ifdef USE_TLS VAR_SMTPD_TLS_ACERT, DEF_SMTPD_TLS_ACERT, &var_smtpd_tls_ask_ccert, VAR_SMTPD_TLS_RCERT, DEF_SMTPD_TLS_RCERT, &var_smtpd_tls_req_ccert, + VAR_SMTPD_TLS_ENABLE_RPK, DEF_SMTPD_TLS_ENABLE_RPK, &var_smtpd_tls_enable_rpk, VAR_SMTPD_TLS_RECHEAD, DEF_SMTPD_TLS_RECHEAD, &var_smtpd_tls_received_header, VAR_SMTPD_TLS_SET_SESSID, DEF_SMTPD_TLS_SET_SESSID, &var_smtpd_tls_set_sessid, #endif diff --git a/src/smtpd/smtpd.h b/src/smtpd/smtpd.h index 56ebc07..c049194 100644 --- a/src/smtpd/smtpd.h +++ b/src/smtpd/smtpd.h @@ -114,6 +114,7 @@ typedef struct { int junk_cmds; /* counter */ int rcpt_overshoot; /* counter */ char *rewrite_context; /* address rewriting context */ + int notes; /* notes aggregator */ /* * SASL specific. @@ -209,6 +210,8 @@ typedef struct { #define SMTPD_FLAG_SMTPUTF8 (1<<3) /* RFC 6531/2 transaction */ #define SMTPD_FLAG_NEED_MILTER_ABORT (1<<4) /* undo milter_mail_event() */ +#define SMTPD_NOTE_BARE_LF (1<<0) /* saw at least one bare LF */ + /* Security: don't reset SMTPD_FLAG_AUTH_USED. */ #define SMTPD_MASK_MAIL_KEEP \ ~(SMTPD_FLAG_SMTPUTF8) /* Fix 20140706 */ @@ -260,6 +263,7 @@ extern void smtpd_state_reset(SMTPD_STATE *); #define SMTPD_CMD_XCLIENT "XCLIENT" #define SMTPD_CMD_XFORWARD "XFORWARD" #define SMTPD_CMD_UNKNOWN "UNKNOWN" +#define SMTPD_CMD_HELP "HELP" /* * Representation of unknown and non-existent client information. Throughout diff --git a/src/smtpd/smtpd_check.c b/src/smtpd/smtpd_check.c index 093aa06..6aeda74 100644 --- a/src/smtpd/smtpd_check.c +++ b/src/smtpd/smtpd_check.c @@ -1598,6 +1598,7 @@ static int permit_auth_destination(SMTPD_STATE *state, char *recipient); static int permit_tls_clientcerts(SMTPD_STATE *state, int permit_all_certs) { #ifdef USE_TLS + const char *myname = "permit_tls_clientcerts"; const char *found = 0; if (!state->tls_context) @@ -1612,9 +1613,9 @@ static int permit_tls_clientcerts(SMTPD_STATE *state, int permit_all_certs) /* * When directly checking the fingerprint, it is OK if the issuing CA is - * not trusted. + * not trusted. Raw public keys are also acceptable. */ - if (TLS_CERT_IS_PRESENT(state->tls_context)) { + if (TLS_CRED_IS_PRESENT(state->tls_context)) { int i; char *prints[2]; @@ -1623,12 +1624,24 @@ static int permit_tls_clientcerts(SMTPD_STATE *state, int permit_all_certs) VAR_SMTPD_TLS_FPT_DGST "=md5 to compute certificate " "fingerprints"); - prints[0] = state->tls_context->peer_cert_fprint; - prints[1] = state->tls_context->peer_pkey_fprint; + prints[0] = state->tls_context->peer_pkey_fprint; + prints[1] = state->tls_context->peer_cert_fprint; /* After lookup error, leave relay_ccerts->error at non-zero value. */ for (i = 0; i < 2; ++i) { + /* With RFC7250 RPK, no certificate may be available */ + if (!*prints[i]) + continue; found = maps_find(relay_ccerts, prints[i], DICT_FLAG_NONE); + if (var_smtpd_tls_enable_rpk && i > 0 && found) { + msg_warn("%s: %s: %s: Fragile access policy: %s=yes, but" + " public key fingerprint \"%s\" not matched, while" + " certificate fingerprint \"%s\" matched", + myname, state->namaddr, relay_ccerts->title, + VAR_SMTPD_TLS_ENABLE_RPK, + state->tls_context->peer_cert_fprint, + state->tls_context->peer_pkey_fprint); + } if (found != 0) { if (msg_verbose) msg_info("Relaying allowed for certified client: %s", found); @@ -1659,44 +1672,16 @@ static int check_relay_domains(SMTPD_STATE *state, char *recipient, { const char *myname = "check_relay_domains"; -#if 1 - static int once; - - if (once == 0) { - once = 1; - msg_warn("support for restriction \"%s\" will be removed from %s; " - "use \"%s\" instead", - CHECK_RELAY_DOMAINS, var_mail_name, REJECT_UNAUTH_DEST); - } -#endif - - if (msg_verbose) - msg_info("%s: %s", myname, recipient); - /* - * Permit if the client matches the relay_domains list. + * Restriction check_relay_domains is deprecated as of Postfix 2.2. */ - if (domain_list_match(relay_domains, state->name)) { - if (warn_compat_break_relay_domains) - msg_info("using backwards-compatible default setting " - VAR_RELAY_DOMAINS "=$mydestination to permit " - "request from client \"%s\"", state->name); - return (SMTPD_CHECK_OK); - } - - /* - * Permit authorized destinations. - */ - if (permit_auth_destination(state, recipient) == SMTPD_CHECK_OK) - return (SMTPD_CHECK_OK); + if (msg_verbose) + msg_info("%s: %s", myname, recipient); - /* - * Deny relaying between sites that both are not in relay_domains. - */ - return (smtpd_check_reject(state, MAIL_ERROR_POLICY, - var_relay_code, "5.7.1", - "<%s>: %s rejected: Relay access denied", - reply_name, reply_class)); + msg_warn("support for restriction \"%s\" has been removed in %s 3.9; " + "instead, specify \"%s\"", + CHECK_RELAY_DOMAINS, var_mail_name, REJECT_UNAUTH_DEST); + reject_server_error(state); } /* permit_auth_destination - OK for message relaying */ @@ -2002,11 +1987,22 @@ static int permit_mx_backup(SMTPD_STATE *state, const char *recipient, DNS_RR *middle; DNS_RR *rest; int dns_status; + static int once; if (msg_verbose) msg_info("%s: %s", myname, recipient); /* + * Restriction permit_mx_backup is deprecated as of Postfix 3.9. + */ + if (once == 0) { + once = 1; + msg_warn("support for restriction \"%s\" will be removed from %s; " + "instead, specify \"%s\"", + PERMIT_MX_BACKUP, var_mail_name, VAR_RELAY_DOMAINS); + } + + /* * Resolve the address. */ reply = smtpd_resolve_addr(state->sender, recipient); @@ -3185,6 +3181,9 @@ static int check_ccert_access(SMTPD_STATE *state, const char *acl_spec, #ifdef USE_TLS const char *myname = "check_ccert_access"; + int cert_result = SMTPD_CHECK_DUNNO; + int pkey_result = SMTPD_CHECK_DUNNO; + int *respt; int found; const MAP_SEARCH *acl; const char default_search[] = { @@ -3211,9 +3210,9 @@ static int check_ccert_access(SMTPD_STATE *state, const char *acl_spec, /* * When directly checking the fingerprint, it is OK if the issuing CA is - * not trusted. + * not trusted. Raw public keys are also acceptable. */ - if (TLS_CERT_IS_PRESENT(state->tls_context)) { + if (TLS_CRED_IS_PRESENT(state->tls_context)) { const char *action; const char *match_this; const char *known_action; @@ -3222,17 +3221,19 @@ static int check_ccert_access(SMTPD_STATE *state, const char *acl_spec, switch (*action) { case SMTPD_ACL_SEARCH_CODE_CERT_FPRINT: match_this = state->tls_context->peer_cert_fprint; - if (warn_compat_break_smtpd_tls_fpt_dgst) + if (*match_this && warn_compat_break_smtpd_tls_fpt_dgst) msg_info("using backwards-compatible default setting " VAR_SMTPD_TLS_FPT_DGST "=md5 to compute " "certificate fingerprints"); + respt = &cert_result; break; case SMTPD_ACL_SEARCH_CODE_PKEY_FPRINT: match_this = state->tls_context->peer_pkey_fprint; - if (warn_compat_break_smtpd_tls_fpt_dgst) + if (*match_this && warn_compat_break_smtpd_tls_fpt_dgst) msg_info("using backwards-compatible default setting " VAR_SMTPD_TLS_FPT_DGST "=md5 to compute " - "certificate fingerprints"); + "public key fingerprints"); + respt = &pkey_result; break; default: known_action = str_name_code(search_actions, *action); @@ -3245,6 +3246,9 @@ static int check_ccert_access(SMTPD_STATE *state, const char *acl_spec, 451, "4.3.5", "Server configuration error")); } + /* With RFC7250 RPK, no certificate may be available */ + if (!*match_this) + continue; if (msg_verbose) msg_info("%s: look up %s %s", myname, str_name_code(search_actions, *action), @@ -3257,11 +3261,16 @@ static int check_ccert_access(SMTPD_STATE *state, const char *acl_spec, * "reject" event. XXX Should log the thing that is rejected * (fingerprint etc.) or would that give away too much? */ - result = check_access(state, acl->map_type_name, match_this, + *respt = check_access(state, acl->map_type_name, match_this, DICT_FLAG_NONE, &found, state->tls_context->peer_CN, SMTPD_NAME_CCERT, def_acl); - if (result != SMTPD_CHECK_DUNNO) + if (*respt == SMTPD_CHECK_DUNNO) + continue; + if (result == SMTPD_CHECK_DUNNO) + result = *respt; + if (!var_smtpd_tls_enable_rpk + || *action == SMTPD_ACL_SEARCH_CODE_PKEY_FPRINT) break; } } else if (!var_smtpd_tls_ask_ccert) { @@ -3271,6 +3280,17 @@ static int check_ccert_access(SMTPD_STATE *state, const char *acl_spec, if (msg_verbose) msg_info("%s: no client certificate", myname); } + if (var_smtpd_tls_enable_rpk + && cert_result != SMTPD_CHECK_DUNNO + && cert_result != pkey_result) { + msg_warn("%s: %s: %s: Fragile access policy: %s=yes, but" + " the action for certificate fingerprint \"%s\" !=" + " the action for public key fingerprint \"%s\"", + myname, state->namaddr, acl->map_type_name, + VAR_SMTPD_TLS_ENABLE_RPK, + state->tls_context->peer_cert_fprint, + state->tls_context->peer_pkey_fprint); + } #endif return (result); } @@ -3877,34 +3897,18 @@ static int permit_dnswl_domain(SMTPD_STATE *state, const char *dnswl_domain, static int reject_maps_rbl(SMTPD_STATE *state) { const char *myname = "reject_maps_rbl"; - char *saved_domains = mystrdup(var_maps_rbl_domains); - char *bp = saved_domains; - char *rbl_domain; - int result = SMTPD_CHECK_DUNNO; - static int warned; if (msg_verbose) msg_info("%s: %s", myname, state->addr); - if (warned == 0) { - warned++; - msg_warn("support for restriction \"%s\" will be removed from %s; " - "use \"%s domain-name\" instead", - REJECT_MAPS_RBL, var_mail_name, REJECT_RBL_CLIENT); - } - while ((rbl_domain = mystrtok(&bp, CHARS_COMMA_SP)) != 0) { - result = reject_rbl_addr(state, rbl_domain, state->addr, - SMTPD_NAME_CLIENT); - if (result != SMTPD_CHECK_DUNNO) - break; - } - /* - * Clean up. + * Restriction reject_maps_rbl is deprecated as of Postfix 2.1. */ - myfree(saved_domains); + msg_warn("support for restriction \"%s\" has been removed in %s 3.9; " + "instead, specify \"%s domain-name\"", + REJECT_MAPS_RBL, var_mail_name, REJECT_RBL_CLIENT); - return (result); + reject_server_error(state); } #ifdef USE_SASL_AUTH @@ -3980,7 +3984,7 @@ static int valid_utf8_action(const char *server, const char *action) { int retval; - if ((retval = valid_utf8_string(action, strlen(action))) == 0) + if ((retval = valid_utf8_stringz(action)) == 0) msg_warn("malformed UTF-8 in policy server %s response: \"%s\"", server, action); return (retval); @@ -4035,6 +4039,8 @@ static int check_policy_service(SMTPD_STATE *state, const char *server, ENCODE_CN(subject, subject_buf, state->tls_context->peer_CN); ENCODE_CN(issuer, issuer_buf, state->tls_context->issuer_CN); +#define NONEMPTY(x) ((x) != 0 && (*x) != 0) + /* * XXX: Too noisy to warn for each policy lookup, especially because we * don't even know whether the policy server will use the fingerprint. So @@ -4044,12 +4050,12 @@ static int check_policy_service(SMTPD_STATE *state, const char *server, if (!warned && warn_compat_break_smtpd_tls_fpt_dgst && state->tls_context - && state->tls_context->peer_cert_fprint - && *state->tls_context->peer_cert_fprint) { + && (NONEMPTY(state->tls_context->peer_cert_fprint) + || NONEMPTY(state->tls_context->peer_pkey_fprint))) { warned = 1; msg_info("using backwards-compatible default setting " VAR_SMTPD_TLS_FPT_DGST "=md5 to compute certificate " - "fingerprints"); + "and public key fingerprints"); } #endif @@ -4480,15 +4486,12 @@ static int generic_checks(SMTPD_STATE *state, ARGV *restrictions, state->helo_name, SMTPD_NAME_HELO); } } else if (strcasecmp(name, PERMIT_NAKED_IP_ADDR) == 0) { - msg_warn("restriction %s is deprecated. Use %s or %s instead", - PERMIT_NAKED_IP_ADDR, PERMIT_MYNETWORKS, PERMIT_SASL_AUTH); - if (state->helo_name) { - if (state->helo_name[strspn(state->helo_name, "0123456789.:")] == 0 - && (status = reject_invalid_hostaddr(state, state->helo_name, - state->helo_name, SMTPD_NAME_HELO)) == 0) - status = smtpd_acl_permit(state, name, SMTPD_NAME_HELO, - state->helo_name, NO_PRINT_ARGS); - } + /* permit_naked_ip_addr is deprecated as of Postfix 2.0. */ + msg_warn("support for restriction \"%s\" has been removed in %s" + " 3.9; instead, specify \"%s\" or \"%s\"", + PERMIT_NAKED_IP_ADDR, var_mail_name, + PERMIT_MYNETWORKS, PERMIT_SASL_AUTH); + reject_server_error(state); } else if (is_map_command(state, name, CHECK_HELO_NS_ACL, &cpp)) { if (state->helo_name) { status = check_server_access(state, *cpp, state->helo_name, @@ -5255,8 +5258,9 @@ static int check_recipient_rcpt_maps(SMTPD_STATE *state, const char *recipient) { /* - * Duplicate suppression. There's an implicit check_recipient_maps - * restriction at the end of all recipient restrictions. + * Duplicate suppression. With "smtpd_reject_unlisted_recipient = yes", + * there's an implicit reject_unlisted_recipient restriction at the end + * of all recipient restrictions. */ if (smtpd_input_transp_mask & INPUT_TRANSP_UNKNOWN_RCPT) return (0); @@ -5275,8 +5279,9 @@ static int check_sender_rcpt_maps(SMTPD_STATE *state, const char *sender) { /* - * Duplicate suppression. There's an implicit check_sender_maps - * restriction at the end of all sender restrictions. + * Duplicate suppression. With "smtpd_reject_unlisted_sender = yes", + * there's an implicit reject_unlisted_sender restriction at the end of + * all sender restrictions. */ if (smtpd_input_transp_mask & INPUT_TRANSP_UNKNOWN_RCPT) return (0); @@ -5832,6 +5837,7 @@ char *var_smtpd_dns_re_filter; bool var_smtpd_tls_ask_ccert; int var_smtpd_cipv4_prefix; int var_smtpd_cipv6_prefix; +bool var_smtpd_tls_enable_rpk; #define int_table test_int_table @@ -5869,6 +5875,7 @@ static const INT_TABLE int_table[] = { VAR_SMTPD_TLS_ACERT, DEF_SMTPD_TLS_ACERT, &var_smtpd_tls_ask_ccert, VAR_SMTPD_CIPV4_PREFIX, DEF_SMTPD_CIPV4_PREFIX, &var_smtpd_cipv4_prefix, VAR_SMTPD_CIPV6_PREFIX, DEF_SMTPD_CIPV6_PREFIX, &var_smtpd_cipv6_prefix, + VAR_SMTPD_TLS_ENABLE_RPK, DEF_SMTPD_TLS_ENABLE_RPK, &var_smtpd_tls_enable_rpk, 0, }; @@ -6406,7 +6413,7 @@ int main(int argc, char **argv) state.tls_context->peer_cert_fprint = state.tls_context->peer_pkey_fprint = 0; } - state.tls_context->peer_status |= TLS_CERT_FLAG_PRESENT; + state.tls_context->peer_status |= TLS_CRED_FLAG_CERT; UPDATE_STRING(state.tls_context->peer_cert_fprint, args->argv[1]); state.tls_context->peer_pkey_fprint = diff --git a/src/smtpd/smtpd_check_backup.ref b/src/smtpd/smtpd_check_backup.ref index 8f4a0f2..c15be35 100644 --- a/src/smtpd/smtpd_check_backup.ref +++ b/src/smtpd/smtpd_check_backup.ref @@ -17,6 +17,7 @@ OK >>> recipient_restrictions permit_mx_backup,reject OK >>> rcpt wietse@wzv.porcupine.org +./smtpd_check: warning: support for restriction "permit_mx_backup" will be removed from Postfix; instead, use "relay_domains" OK >>> rcpt wietse@backup.porcupine.org OK diff --git a/src/smtpd/smtpd_deprecated.in b/src/smtpd/smtpd_deprecated.in new file mode 100644 index 0000000..345ee71 --- /dev/null +++ b/src/smtpd/smtpd_deprecated.in @@ -0,0 +1,20 @@ +# +# permit_naked_ip_address +# +client foo 127.0.0.2 +recipient_restrictions permit_naked_ip_address +helo 127.0.0.2 +mail sname@sdomain.example +rcpt rname@rdomain.example +# +# check_relay_domains +# +client foo 127.0.0.2 +recipient_restrictions check_relay_domains +relay_domains foo +helo 127.0.0.2 +mail sname@sdomain.example +rcpt rname@rdomain.example +# +# reject_maps_rbl is already covered elsewhere. +# diff --git a/src/smtpd/smtpd_deprecated.ref b/src/smtpd/smtpd_deprecated.ref new file mode 100644 index 0000000..d64f1b3 --- /dev/null +++ b/src/smtpd/smtpd_deprecated.ref @@ -0,0 +1,35 @@ +>>> # +>>> # permit_naked_ip_address +>>> # +>>> client foo 127.0.0.2 +OK +>>> recipient_restrictions permit_naked_ip_address +OK +>>> helo 127.0.0.2 +OK +>>> mail sname@sdomain.example +OK +>>> rcpt rname@rdomain.example +./smtpd_check: warning: restriction permit_naked_ip_address has been removed in Postfix 3.9; use permit_mynetworks or permit_sasl_authenticated instead +./smtpd_check: <queue id>: reject: RCPT from foo[127.0.0.2]: 451 4.3.5 Server configuration error; from=<sname@sdomain.example> to=<rname@rdomain.example> proto=SMTP helo=<127.0.0.2> +451 4.3.5 Server configuration error +>>> # +>>> # check_relay_domains +>>> # +>>> client foo 127.0.0.2 +OK +>>> recipient_restrictions check_relay_domains +OK +>>> relay_domains foo +OK +>>> helo 127.0.0.2 +OK +>>> mail sname@sdomain.example +OK +>>> rcpt rname@rdomain.example +./smtpd_check: warning: support for restriction "check_relay_domains" has been removed in Postfix 3.9; use "reject_unauth_destination" instead +./smtpd_check: <queue id>: reject: RCPT from foo[127.0.0.2]: 451 4.3.5 Server configuration error; from=<sname@sdomain.example> to=<rname@rdomain.example> proto=SMTP helo=<127.0.0.2> +451 4.3.5 Server configuration error +>>> # +>>> # reject_maps_rbl is already covered elsewhere. +>>> # diff --git a/src/smtpd/smtpd_exp.ref b/src/smtpd/smtpd_exp.ref index 22c027e..00848a5 100644 --- a/src/smtpd/smtpd_exp.ref +++ b/src/smtpd/smtpd_exp.ref @@ -25,13 +25,15 @@ OK >>> client spike.porcupine.org 168.100.3.2 OK >>> rcpt rname@rdomain -./smtpd_check: warning: support for restriction "reject_maps_rbl" will be removed from Postfix; use "reject_rbl_client domain-name" instead -OK +./smtpd_check: warning: support for restriction "reject_maps_rbl" has been removed in Postfix 3.9; use "reject_rbl_client domain-name" instead +./smtpd_check: <queue id>: reject: RCPT from spike.porcupine.org[168.100.3.2]: 451 4.3.5 Server configuration error; from=<sname@sdomain> to=<rname@rdomain> proto=SMTP helo=<foobar> +451 4.3.5 Server configuration error >>> client foo 127.0.0.2 OK >>> rcpt rname@rdomain -./smtpd_check: <queue id>: reject: RCPT from foo[127.0.0.2]: 554 5.7.1 Service unavailable; Client host [127.0.0.2] blocked using dnsbltest.porcupine.org; DNS blocklist test; from=<sname@sdomain> to=<rname@rdomain> proto=SMTP helo=<foobar> -554 5.7.1 Service unavailable; Client host [127.0.0.2] blocked using dnsbltest.porcupine.org; DNS blocklist test +./smtpd_check: warning: support for restriction "reject_maps_rbl" has been removed in Postfix 3.9; use "reject_rbl_client domain-name" instead +./smtpd_check: <queue id>: reject: RCPT from foo[127.0.0.2]: 451 4.3.5 Server configuration error; from=<sname@sdomain> to=<rname@rdomain> proto=SMTP helo=<foobar> +451 4.3.5 Server configuration error >>> # >>> recipient_restrictions reject_rbl_client,dnsbltest.porcupine.org OK diff --git a/src/smtpd/smtpd_sasl_glue.c b/src/smtpd/smtpd_sasl_glue.c index d9db7b0..1163366 100644 --- a/src/smtpd/smtpd_sasl_glue.c +++ b/src/smtpd/smtpd_sasl_glue.c @@ -120,6 +120,10 @@ /* Google, Inc. /* 111 8th Avenue /* New York, NY 10011, USA +/* +/* Wietse Venema +/* porcupine.org +/* Amawalk, NY 10501, USA /*--*/ /* System library. */ diff --git a/src/smtpd/smtpd_state.c b/src/smtpd/smtpd_state.c index f2f5f89..fefc543 100644 --- a/src/smtpd/smtpd_state.c +++ b/src/smtpd/smtpd_state.c @@ -135,6 +135,7 @@ void smtpd_state_init(SMTPD_STATE *state, VSTREAM *stream, state->instance = vstring_alloc(10); state->seqno = 0; state->rewrite_context = 0; + state->notes = 0; #if 0 state->ehlo_discard_mask = ~0; #else diff --git a/src/smtpstone/smtp-source.c b/src/smtpstone/smtp-source.c index be388d6..f9fa64f 100644 --- a/src/smtpstone/smtp-source.c +++ b/src/smtpstone/smtp-source.c @@ -42,7 +42,7 @@ /* Don't disconnect after sending a message; send the next /* message over the same connection. /* .IP "\fB-f \fIfrom\fR" -/* Use the specified sender address (default: <foo@myhostname>). +/* Use the specified sender address (default: <foo@my-hostname>). /* .IP "\fB-F \fIfile\fR" /* Send the pre-formatted message header and body in the /* specified \fIfile\fR, while prepending '.' before lines that @@ -54,31 +54,49 @@ /* Speak LMTP rather than SMTP. /* .IP "\fB-m \fImessage_count\fR" /* Send the specified number of messages (default: 1). -/* .IP "\fB-M \fImyhostname\fR" +/* .IP "\fB-M \fImy-hostname\fR" /* Use the specified hostname or [address] in the HELO command /* and in the default sender and recipient addresses, instead /* of the machine hostname. /* .IP "\fB-N\fR" -/* Prepend a non-repeating sequence number to each recipient -/* address. This avoids the artificial 100% hit rate in the -/* resolve and rewrite client caches and exercises the -/* trivial-rewrite daemon, better approximating Postfix -/* performance under real-life work-loads. +/* Generate each recipient address by appending a number (a +/* per-process recipient counter) to the recipient address +/* localpart specified with the \fB-t\fR option. +/* +/* Note: to use the number as an address extension, specify +/* an explicit address delimiter at the end of the recipient +/* localpart, as in "\fB-t localpart+@domain\fR" or "\fB-t +/* localpart+\fR", where "\fB+\fR" is a Postfix recipient +/* address delimiter. +/* +/* Benefits: +/* .RS +/* .IP \(bu +/* A non-constant recipient address avoids an unrealistic 100% +/* cache hit rate in clients of the Postfix trivial-rewrite +/* service, better approximating performance under real-life +/* work-loads. +/* .IP \(bu +/* A fixed recipient address local-part with a non-constant +/* address extension avoids the need to configure a large +/* number of valid recipient addresses in the receiving Postfix +/* server. +/* .RE /* .IP \fB-o\fR /* Old mode: don't send HELO, and don't send message headers. /* .IP "\fB-r \fIrecipient_count\fR" -/* Send the specified number of recipients per transaction (default: 1). -/* Recipient names are generated by prepending a number to the -/* recipient address. +/* Send the specified number of recipients per transaction +/* (default: 1), and generate recipient addresses as described +/* under the \fB-N\fR option. /* .IP "\fB-R \fIinterval\fR" -/* Wait for a random period of time 0 <= n <= interval between messages. +/* Wait a random time (0 <= n <= \fIinterval\fR) between messages. /* Suspending one thread does not affect other delivery threads. /* .IP "\fB-s \fIsession_count\fR" /* Run the specified number of SMTP sessions in parallel (default: 1). /* .IP "\fB-S \fIsubject\fR" /* Send mail with the named subject line (default: none). /* .IP "\fB-t \fIto\fR" -/* Use the specified recipient address (default: <foo@myhostname>). +/* Use the specified recipient address (default: <foo@my-hostname>). /* .IP "\fB-T \fIwindowsize\fR" /* Override the default TCP window size. To work around /* broken TCP window scaling implementations, specify a @@ -172,6 +190,7 @@ typedef struct SESSION { int rcpt_done; /* # of recipients done */ int rcpt_count; /* # of recipients to go */ int rcpt_accepted; /* # of recipients accepted */ + int rcpt_sample; /* Sample recipient # for To: header */ VSTREAM *stream; /* open connection */ int connect_count; /* # of connect()s to retry */ struct SESSION *next; /* connect() queue linkage */ @@ -202,7 +221,11 @@ static struct sockaddr *sa; static int sa_length; static int recipients = 1; static char *defaddr; -static char *recipient; +typedef struct { + char *local; + char *at_domain; +} RECIPIENT; +static RECIPIENT *recipient; static char *sender; static char *message_data; static int message_length; @@ -216,7 +239,8 @@ static int random_delay = 0; static int fixed_delay = 0; static int talk_lmtp = 0; static char *subject = 0; -static int number_rcpts = 0; +static int global_rcpt_suffix = 0; +static int global_rcpt_done = 0; static int allow_reject = 0; static void enqueue_connect(SESSION *); @@ -238,6 +262,20 @@ static void send_quit(SESSION *); static void quit_done(int, void *); static void close_session(SESSION *); +/* make_recipient - parse recipient into localpart and at_domain */ + +static RECIPIENT *make_recipient(const char *address) +{ + RECIPIENT *rp = (RECIPIENT *) mymalloc(sizeof(*rp)); + const char *at; + + if ((at = strrchr(address, '@')) == 0) + at = address + strlen(address); + rp->local = mystrndup(address, at - address); + rp->at_domain = mystrdup(at); + return (rp); +} + /* random_interval - generate a random value in 0 .. (small) interval */ static int random_interval(int interval) @@ -655,12 +693,13 @@ static void send_rcpt(int unused_event, void *context) if ((except = vstream_setjmp(session->stream)) != 0) msg_fatal("%s while sending recipient", exception_text(except)); - if (session->rcpt_count > 1 || number_rcpts > 0) - command(session->stream, "RCPT TO:<%d%s>", - number_rcpts ? number_rcpts++ : session->rcpt_count, - recipient); + if (global_rcpt_suffix) + command(session->stream, "RCPT TO:<%s%d%s>", + recipient->local, session->rcpt_sample = global_rcpt_done++, + recipient->at_domain); else - command(session->stream, "RCPT TO:<%s>", recipient); + command(session->stream, "RCPT TO:<%s%s>", + recipient->local, recipient->at_domain); session->rcpt_count--; session->rcpt_done++; @@ -765,10 +804,16 @@ static void data_done(int unused, void *context) mypid = getpid(); } smtp_printf(session->stream, "From: <%s>", sender); - smtp_printf(session->stream, "To: <%s>", recipient); + if (global_rcpt_suffix) + smtp_printf(session->stream, "To: <%s%d%s>", recipient->local, + session->rcpt_sample, recipient->at_domain); + else + smtp_printf(session->stream, "To: <%s%s>", + recipient->local, recipient->at_domain); smtp_printf(session->stream, "Date: %s", mydate); smtp_printf(session->stream, "Message-Id: <%04x.%04x.%04x@%s>", - mypid, vstream_fileno(session->stream), message_count, var_myhostname); + mypid, vstream_fileno(session->stream), message_count, + var_myhostname); if (subject) smtp_printf(session->stream, "Subject: %s", subject); smtp_fputs("", 0, session->stream); @@ -1021,7 +1066,7 @@ int main(int argc, char **argv) var_myhostname = optarg; break; case 'N': - number_rcpts = 1; + global_rcpt_suffix = 1; break; case 'o': send_helo_first = 0; @@ -1030,6 +1075,7 @@ int main(int argc, char **argv) case 'r': if ((recipients = atoi(optarg)) <= 0) msg_fatal("bad recipient count: %s", optarg); + global_rcpt_suffix = 1; break; case 'R': if (fixed_delay > 0) @@ -1045,7 +1091,7 @@ int main(int argc, char **argv) subject = optarg; break; case 't': - recipient = optarg; + recipient = make_recipient(optarg); break; case 'T': if ((inet_windowsize = atoi(optarg)) <= 0) @@ -1160,7 +1206,7 @@ int main(int argc, char **argv) if (sender == 0) sender = defaddr; if (recipient == 0) - recipient = defaddr; + recipient = make_recipient(defaddr); } /* diff --git a/src/tls/tls.h b/src/tls/tls.h index 73eebae..3ec41ba 100644 --- a/src/tls/tls.h +++ b/src/tls/tls.h @@ -78,6 +78,7 @@ extern const char *str_tls_level(int); #include <openssl/opensslv.h> /* OPENSSL_VERSION_NUMBER */ #include <openssl/ssl.h> #include <openssl/conf.h> +#include <openssl/tls1.h> /* TLS extensions */ /* Appease indent(1) */ #define x509_stack_t STACK_OF(X509) @@ -203,8 +204,8 @@ extern void tls_dane_flush(void); extern TLS_DANE *tls_dane_alloc(void); extern void tls_tlsa_free(TLS_TLSA *); extern void tls_dane_free(TLS_DANE *); -extern void tls_dane_add_fpt_digests(TLS_DANE *, const char *, const char *, - int); +extern void tls_dane_add_fpt_digests(TLS_DANE *, int, const char *, + const char *, int); extern TLS_DANE *tls_dane_resolve(unsigned, const char *, DNS_RR *, int); extern int tls_dane_load_trustfile(TLS_DANE *, const char *); @@ -232,6 +233,8 @@ typedef struct { const char *kex_name; /* shared key-exchange algorithm */ const char *kex_curve; /* shared key-exchange ECDHE curve */ int kex_bits; /* shared FFDHE key exchange bits */ + int ctos_rpk; /* Did the client send an RPK? */ + int stoc_rpk; /* Did the server send an RPK? */ const char *clnt_sig_name; /* client's signature key algorithm */ const char *clnt_sig_curve; /* client's ECDSA curve name */ int clnt_sig_bits; /* client's RSA signature key bits */ @@ -264,13 +267,17 @@ typedef struct { * Peer status bits. TLS_CERT_FLAG_MATCHED implies TLS_CERT_FLAG_TRUSTED * only in the case of a hostname match. */ -#define TLS_CERT_FLAG_PRESENT (1<<0) +#define TLS_CRED_FLAG_CERT (1<<0) #define TLS_CERT_FLAG_ALTNAME (1<<1) #define TLS_CERT_FLAG_TRUSTED (1<<2) #define TLS_CERT_FLAG_MATCHED (1<<3) #define TLS_CERT_FLAG_SECURED (1<<4) +#define TLS_CRED_FLAG_RPK (1<<5) +#define TLS_CRED_FLAG_ANY (TLS_CRED_FLAG_CERT|TLS_CRED_FLAG_RPK) -#define TLS_CERT_IS_PRESENT(c) ((c) && ((c)->peer_status&TLS_CERT_FLAG_PRESENT)) +#define TLS_CRED_IS_PRESENT(c) ((c) && ((c)->peer_status&TLS_CRED_FLAG_ANY)) +#define TLS_CERT_IS_PRESENT(c) ((c) && ((c)->peer_status&TLS_CRED_FLAG_CERT)) +#define TLS_RPK_IS_PRESENT(c) ((c) && ((c)->peer_status&TLS_CRED_FLAG_RPK)) #define TLS_CERT_IS_ALTNAME(c) ((c) && ((c)->peer_status&TLS_CERT_FLAG_ALTNAME)) #define TLS_CERT_IS_TRUSTED(c) ((c) && ((c)->peer_status&TLS_CERT_FLAG_TRUSTED)) #define TLS_CERT_IS_MATCHED(c) ((c) && ((c)->peer_status&TLS_CERT_FLAG_MATCHED)) @@ -472,6 +479,7 @@ typedef struct { VSTREAM *stream; int fd; /* Event-driven file descriptor */ int timeout; + int enable_rpk; /* Solicit server raw public keys */ int tls_level; /* Security level */ const char *nexthop; /* destination domain */ const char *host; /* MX hostname */ @@ -508,12 +516,12 @@ extern TLS_SESS_STATE *tls_client_post_connect(TLS_SESS_STATE *, a6, a7, a8, a9, a10, a11, a12, a13, a14)) #define TLS_CLIENT_START(props, a1, a2, a3, a4, a5, a6, a7, a8, a9, \ - a10, a11, a12, a13, a14, a15, a16, a17) \ + a10, a11, a12, a13, a14, a15, a16, a17, a18) \ tls_client_start((((props)->a1), ((props)->a2), ((props)->a3), \ ((props)->a4), ((props)->a5), ((props)->a6), ((props)->a7), \ ((props)->a8), ((props)->a9), ((props)->a10), ((props)->a11), \ ((props)->a12), ((props)->a13), ((props)->a14), ((props)->a15), \ - ((props)->a16), ((props)->a17), (props))) + ((props)->a16), ((props)->a17), ((props)->a18), (props))) /* * tls_server.c @@ -546,6 +554,7 @@ typedef struct { VSTREAM *stream; /* Client stream */ int fd; /* Event-driven file descriptor */ int timeout; /* TLS handshake timeout */ + int enable_rpk; /* Solicit client raw public keys */ int requirecert; /* Insist on client cert? */ const char *serverid; /* Server instance (salt cache key) */ const char *namaddr; /* Client nam[addr] for logging */ @@ -570,10 +579,12 @@ extern TLS_SESS_STATE *tls_server_post_accept(TLS_SESS_STATE *); ((props)->a16), ((props)->a17), ((props)->a18), ((props)->a19), \ ((props)->a20), (props))) -#define TLS_SERVER_START(props, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) \ +#define TLS_SERVER_START(props, a1, a2, a3, a4, a5, a6, a7, a8, a9, \ + a10, a11) \ tls_server_start((((props)->a1), ((props)->a2), ((props)->a3), \ ((props)->a4), ((props)->a5), ((props)->a6), ((props)->a7), \ - ((props)->a8), ((props)->a9), ((props)->a10), (props))) + ((props)->a8), ((props)->a9), ((props)->a10), ((props)->a11), \ + (props))) /* * tls_session.c @@ -660,7 +671,7 @@ extern TLS_TLSA *tlsa_prepend(TLS_TLSA *, uint8_t, uint8_t, uint8_t, extern const EVP_MD *tls_digest_byname(const char *, EVP_MD_CTX **); extern char *tls_digest_encode(const unsigned char *, int); extern char *tls_cert_fprint(X509 *, const char *); -extern char *tls_pkey_fprint(X509 *, const char *); +extern char *tls_pkey_fprint(EVP_PKEY *, const char *); extern char *tls_serverid_digest(TLS_SESS_STATE *, const TLS_CLIENT_START_PROPS *, const char *); @@ -696,6 +707,8 @@ extern long tls_bio_dump_cb(BIO *, int, const char *, int, long, long); #endif extern const EVP_MD *tls_validate_digest(const char *); +extern void tls_enable_client_rpk(SSL_CTX *, SSL *); +extern void tls_enable_server_rpk(SSL_CTX *, SSL *); /* * tls_seed.c diff --git a/src/tls/tls_client.c b/src/tls/tls_client.c index e0dfe15..3eda859 100644 --- a/src/tls/tls_client.c +++ b/src/tls/tls_client.c @@ -86,8 +86,9 @@ /* available as: /* .IP TLScontext->peer_status /* A bitmask field that records the status of the peer certificate -/* verification. This consists of one or more of TLS_CERT_FLAG_PRESENT, -/* TLS_CERT_FLAG_TRUSTED, TLS_CERT_FLAG_MATCHED and TLS_CERT_FLAG_SECURED. +/* verification. This consists of one or more of TLS_CRED_FLAG_CERT, +/* TLS_CRED_FLAG_RPK, TLS_CERT_FLAG_TRUSTED, TLS_CERT_FLAG_MATCHED and +/* TLS_CERT_FLAG_SECURED. /* .IP TLScontext->peer_CN /* Extracted CommonName of the peer, or zero-length string if the /* information could not be extracted. @@ -303,15 +304,11 @@ static void uncache_session(SSL_CTX *ctx, TLS_SESS_STATE *TLScontext) tls_mgr_delete(TLScontext->cache_type, TLScontext->serverid); } -/* verify_extract_name - verify peer name and extract peer information */ +/* verify_x509 - process X.509 certificate verification status */ -static void verify_extract_name(TLS_SESS_STATE *TLScontext, X509 *peercert, - const TLS_CLIENT_START_PROPS *props) +static void verify_x509(TLS_SESS_STATE *TLScontext, X509 *peercert, + const TLS_CLIENT_START_PROPS *props) { - int verbose; - - verbose = TLScontext->log_mask & - (TLS_LOG_CERTMATCH | TLS_LOG_VERBOSE | TLS_LOG_PEERCERT); /* * On exit both peer_CN and issuer_CN should be set. @@ -345,7 +342,8 @@ static void verify_extract_name(TLS_SESS_STATE *TLScontext, X509 *peercert, TLScontext->peer_status |= TLS_CERT_FLAG_SECURED; TLScontext->peer_status |= TLS_CERT_FLAG_MATCHED; - if (verbose) { + if (TLScontext->log_mask & + (TLS_LOG_CERTMATCH | TLS_LOG_VERBOSE | TLS_LOG_PEERCERT)) { const char *peername = SSL_get0_peername(TLScontext->con); if (peername) @@ -367,6 +365,50 @@ static void verify_extract_name(TLS_SESS_STATE *TLScontext, X509 *peercert, if (TLScontext->session_reused == 0) tls_log_verify_error(TLScontext); else + msg_info("%s: re-using session with untrusted peer credential, " + "look for details earlier in the log", props->namaddr); + } +} + +/* verify_rpk - process RFC7250 raw public key verification status */ + +static void verify_rpk(TLS_SESS_STATE *TLScontext, EVP_PKEY *peerpkey, + const TLS_CLIENT_START_PROPS *props) +{ + /* Was the raw public key (type of cert) matched? */ + if (SSL_get_verify_result(TLScontext->con) == X509_V_OK) { + TLScontext->peer_status |= TLS_CERT_FLAG_TRUSTED; + if (TLScontext->must_fail) { + msg_panic("%s: raw public key valid despite trust init failure", + TLScontext->namaddr); + } else if (TLS_MUST_MATCH(TLScontext->level)) { + + /* + * Fully secured only if not insecure like half-dane. We use + * TLS_CERT_FLAG_MATCHED to satisfy policy, but + * TLS_CERT_FLAG_SECURED to log the effective security. + */ + if (!TLS_NEVER_SECURED(TLScontext->level)) + TLScontext->peer_status |= TLS_CERT_FLAG_SECURED; + TLScontext->peer_status |= TLS_CERT_FLAG_MATCHED; + + if (TLScontext->log_mask & + (TLS_LOG_CERTMATCH | TLS_LOG_VERBOSE | TLS_LOG_PEERCERT)) + tls_dane_log(TLScontext); + } + } + + /* + * Give them a clue. Problems with trust chain verification are logged + * when the session is first negotiated, before the session is stored + * into the cache. We don't want mystery failures, so log the fact the + * real problem is to be found in the past. + */ + if (!TLS_CERT_IS_MATCHED(TLScontext) + && (TLScontext->log_mask & TLS_LOG_UNTRUSTED)) { + if (TLScontext->session_reused == 0) + tls_log_verify_error(TLScontext); + else msg_info("%s: re-using session with untrusted certificate, " "look for details earlier in the log", props->namaddr); } @@ -793,6 +835,30 @@ TLS_APPL_STATE *tls_client_init(const TLS_CLIENT_INIT_PROPS *props) } /* + * Enable support for client->server raw public keys, provided we actually + * have keys to send. They'll only be used if the server also enables + * client RPKs. + * + * XXX: When the server requests client auth, the TLS 1.2 protocol does not + * provide an unambiguous mechanism for the client to not send an RPK (as + * it can with client X.509 certs or TLS 1.3). This is why we don't just + * enable client RPK also with no keys in hand. + * + * A very unlikely scenario is that the server allows clients to not send + * keys, but only accepts keys for a set of algorithms we don't have. Then + * we still can't send a key, but have agreed to RPK. OpenSSL will attempt + * to send an empty RPK even with TLS 1.2 (and will accept such a message), + * but other implementations may be more strict. + * + * We could limit client RPK support to connections that support only TLS + * 1.3 and up, but that's practical only decades in the future, and the + * risk scenario is contrived and very unlikely. + */ + if (SSL_CTX_get0_certificate(client_ctx) != NULL && + SSL_CTX_get0_privatekey(client_ctx) != NULL) + tls_enable_client_rpk(client_ctx, NULL); + + /* * With OpenSSL 1.0.2 and later the client EECDH curve list becomes * configurable with the preferred curve negotiated via the supported * curves extension. With OpenSSL 3.0 and TLS 1.3, the same applies @@ -1008,6 +1074,24 @@ TLS_SESS_STATE *tls_client_start(const TLS_CLIENT_START_PROPS *props) } /* + * Possibly enable RFC7250 raw public keys in non-DANE/non-PKI levels + * when the fingerprint mask includes only public keys. For "may" and + * "encrypt" this is a heuristic, since we don't use the fingerprints + * beyond reporting them in verbose logging. If you always want certs + * with "may" and "encrypt" you'll have to tolerate them with + * "fingerprint", or use a separate transport. + */ + switch (props->tls_level) { + case TLS_LEV_MAY: + case TLS_LEV_ENCRYPT: + case TLS_LEV_FPRINT: + if (props->enable_rpk) + tls_enable_server_rpk(NULL, TLScontext->con); + default: + break; + } + + /* * Try to convey the configured TLSA records for this connection to the * OpenSSL library. If none are "usable", we'll fall back to "encrypt" * when authentication is not mandatory, otherwise we must arrange to @@ -1175,6 +1259,7 @@ TLS_SESS_STATE *tls_client_post_connect(TLS_SESS_STATE *TLScontext, { const SSL_CIPHER *cipher; X509 *peercert; + EVP_PKEY *peerpkey = 0; /* Turn off packet dump if only dumping the handshake */ if ((TLScontext->log_mask & TLS_LOG_ALLPKTS) == 0) @@ -1192,31 +1277,61 @@ TLS_SESS_STATE *tls_client_post_connect(TLS_SESS_STATE *TLScontext, * Do peername verification if requested and extract useful information * from the certificate for later use. */ - if ((peercert = TLS_PEEK_PEER_CERT(TLScontext->con)) != 0) { - TLScontext->peer_status |= TLS_CERT_FLAG_PRESENT; + peercert = TLS_PEEK_PEER_CERT(TLScontext->con); + if (peercert != 0) { + peerpkey = X509_get0_pubkey(peercert); + } +#if OPENSSL_VERSION_PREREQ(3,2) + else { + peerpkey = SSL_get0_peer_rpk(TLScontext->con); + } +#endif + + if (peercert != 0) { + TLScontext->peer_status |= TLS_CRED_FLAG_CERT; /* * Peer name or fingerprint verification as requested. * Unconditionally set peer_CN, issuer_CN and peer_cert_fprint. Check * fingerprint first, and avoid logging verified as untrusted in the - * call to verify_extract_name(). + * call to verify_x509(). */ - TLScontext->peer_cert_fprint = tls_cert_fprint(peercert, props->mdalg); - TLScontext->peer_pkey_fprint = tls_pkey_fprint(peercert, props->mdalg); - verify_extract_name(TLScontext, peercert, props); + TLScontext->peer_cert_fprint = + tls_cert_fprint(peercert, props->mdalg); + TLScontext->peer_pkey_fprint = + tls_pkey_fprint(peerpkey, props->mdalg); + verify_x509(TLScontext, peercert, props); if (TLScontext->log_mask & (TLS_LOG_CERTMATCH | TLS_LOG_VERBOSE | TLS_LOG_PEERCERT)) - msg_info("%s: subject_CN=%s, issuer_CN=%s, " - "fingerprint=%s, pkey_fingerprint=%s", props->namaddr, + msg_info("%s: subject_CN=%s, issuer=%s%s%s%s%s", + TLScontext->namaddr, TLScontext->peer_CN, TLScontext->issuer_CN, - TLScontext->peer_cert_fprint, - TLScontext->peer_pkey_fprint); + *TLScontext->peer_cert_fprint ? + ", cert fingerprint=" : "", + *TLScontext->peer_cert_fprint ? + TLScontext->peer_cert_fprint : "", + *TLScontext->peer_pkey_fprint ? + ", pkey fingerprint=" : "", + *TLScontext->peer_pkey_fprint ? + TLScontext->peer_pkey_fprint : ""); } else { TLScontext->issuer_CN = mystrdup(""); TLScontext->peer_CN = mystrdup(""); TLScontext->peer_cert_fprint = mystrdup(""); - TLScontext->peer_pkey_fprint = mystrdup(""); + + if (!peerpkey) { + TLScontext->peer_pkey_fprint = mystrdup(""); + } else { + TLScontext->peer_status |= TLS_CRED_FLAG_RPK; + TLScontext->peer_pkey_fprint = + tls_pkey_fprint(peerpkey, props->mdalg); + if (TLScontext->log_mask & + (TLS_LOG_CERTMATCH | TLS_LOG_VERBOSE | TLS_LOG_PEERCERT)) + msg_info("%s: raw public key fingerprint=%s", props->namaddr, + TLScontext->peer_pkey_fprint); + verify_rpk(TLScontext, peerpkey, props); + } } /* diff --git a/src/tls/tls_dane.c b/src/tls/tls_dane.c index a2b9b80..ac7f05f 100644 --- a/src/tls/tls_dane.c +++ b/src/tls/tls_dane.c @@ -22,8 +22,9 @@ /* void tls_dane_free(dane) /* TLS_DANE *dane; /* -/* void tls_dane_add_fpt_digests(dane, digest, delim, smtp_mode) +/* void tls_dane_add_fpt_digests(dane, pkey_only, digest, delim, smtp_mode) /* TLS_DANE *dane; +/* int pkey_only; /* const char *digest; /* const char *delim; /* int smtp_mode; @@ -130,6 +131,9 @@ /* SSL context to be configured with the chosen digest algorithms. /* .IP fpt_alg /* The OpenSSL EVP digest algorithm handle for the fingerprint digest. +/* .IP pkey_only +/* When true, generate "fingerprint" TLSA records for just the public +/* keys. Otherwise, for both certificates and public keys. /* .IP tlsa /* TLSA record linked list head, initially NULL. /* .IP usage @@ -415,8 +419,9 @@ static void dane_free(void *dane, void *unused_context) /* tls_dane_add_fpt_digests - map fingerprint list to DANE TLSA RRset */ -void tls_dane_add_fpt_digests(TLS_DANE *dane, const char *digest, - const char *delim, int smtp_mode) +void tls_dane_add_fpt_digests(TLS_DANE *dane, int pkey_only, + const char *digest, const char *delim, + int smtp_mode) { ARGV *values = argv_split(digest, delim); ssize_t i; @@ -455,31 +460,41 @@ void tls_dane_add_fpt_digests(TLS_DANE *dane, const char *digest, continue; } +#define USTR_LEN(raw) (unsigned char *) STR(raw), VSTRING_LEN(raw) + /* * At the "fingerprint" security level certificate digests and public - * key digests are interchangeable. Each leaf certificate is matched - * via either the public key digest or full certificate digest. The - * DER encoding of a certificate is not a valid public key, and - * conversely, the DER encoding of a public key is not a valid - * certificate. An attacker would need a 2nd-preimage that is + * key digests are by default interchangeable. Each leaf certificate + * is matched via either the public key digest or full certificate + * digest. The DER encoding of a certificate is not a valid public + * key, and conversely, the DER encoding of a public key is not a + * valid certificate. An attacker would need a 2nd-preimage that is * feasible across types (given cert digest == some pkey digest) and * yet presumably difficult within a type (e.g. given cert digest == * some other cert digest). No such attacks are known at this time, * and it is expected that if any are found they would work within as * well as across the cert/pkey data types. + * + * That said, when `pkey_only` is true, we match only public keys. * * The private-use matching type "255" is mapped to the configured * fingerprint digest, which may (harmlessly) coincide with one of * the standard DANE digest algorithms. The private code point is * however unconditionally enabled. */ + if (!pkey_only) { + dane->tlsa = tlsa_prepend(dane->tlsa, 3, 0, 255, USTR_LEN(raw)); + if (log_mask & (TLS_LOG_VERBOSE | TLS_LOG_DANE)) + tlsa_info("fingerprint", "digest as private-use TLSA record", + 3, 0, 255, USTR_LEN(raw)); + } + + /* The public key match is unconditional */ + dane->tlsa = tlsa_prepend(dane->tlsa, 3, 1, 255, USTR_LEN(raw)); if (log_mask & (TLS_LOG_VERBOSE | TLS_LOG_DANE)) tlsa_info("fingerprint", "digest as private-use TLSA record", - 3, 0, 255, (unsigned char *) STR(raw), VSTRING_LEN(raw)); - dane->tlsa = tlsa_prepend(dane->tlsa, 3, 0, 255, - (unsigned char *) STR(raw), VSTRING_LEN(raw)); - dane->tlsa = tlsa_prepend(dane->tlsa, 3, 1, 255, - (unsigned char *) STR(raw), VSTRING_LEN(raw)); + 3, 1, 255, USTR_LEN(raw)); + vstring_free(raw); } argv_free(values); @@ -798,12 +813,21 @@ int tls_dane_enable(TLS_SESS_STATE *TLScontext) SSL *ssl = TLScontext->con; int usable = 0; int ret; + int rpk_compat = 1; for (tp = dane->tlsa; tp != 0; tp = tp->next) { ret = SSL_dane_tlsa_add(ssl, tp->usage, tp->selector, tp->mtype, tp->data, tp->length); if (ret > 0) { ++usable; + /* + * Disable use of RFC7250 raw public keys if any TLSA record + * depends on X.509 certificates. Only DANE-EE(3) SPKI(1) records + * can get by with just a public key. + */ + if (tp->usage != DNS_TLSA_USAGE_DOMAIN_ISSUED_CERTIFICATE + || tp->selector != DNS_TLSA_SELECTOR_SUBJECTPUBLICKEYINFO) + rpk_compat = 0; continue; } if (ret == 0) { @@ -818,6 +842,9 @@ int tls_dane_enable(TLS_SESS_STATE *TLScontext) tls_print_errors(); return (-1); } + if (rpk_compat) + tls_enable_server_rpk(NULL, ssl); + return (usable); } @@ -964,8 +991,9 @@ void tls_dane_log(TLS_SESS_STATE *TLScontext) { static VSTRING *top; static VSTRING *bot; + X509 *mcert = 0; EVP_PKEY *mspki = 0; - int depth = SSL_get0_dane_authority(TLScontext->con, NULL, &mspki); + int depth = SSL_get0_dane_authority(TLScontext->con, &mcert, &mspki); uint8_t u, s, m; unsigned const char *data; size_t dlen; @@ -994,22 +1022,27 @@ void tls_dane_log(TLS_SESS_STATE *TLScontext) hex_encode(top, (char *) data, dlen); } - switch (TLScontext->level) { - case TLS_LEV_FPRINT: + if (TLScontext->level == TLS_LEV_FPRINT) { msg_info("%s: Matched fingerprint: %s%s%s", TLScontext->namaddr, STR(top), dlen > MAX_DUMP_BYTES ? "..." : "", dlen > MAX_DUMP_BYTES ? STR(bot) : ""); return; - - default: - msg_info("%s: Matched DANE %s at depth %d: %u %u %u %s%s%s", - TLScontext->namaddr, mspki ? - "TA public key verified certificate" : depth ? - "TA certificate" : "EE certificate", depth, u, s, m, + } +#if OPENSSL_VERSION_PREREQ(3,2) + if (SSL_get0_peer_rpk(TLScontext->con) != NULL) { + msg_info("%s: Matched DANE raw public key: %u %u %u %s%s%s", + TLScontext->namaddr, u, s, m, STR(top), dlen > MAX_DUMP_BYTES ? "..." : "", dlen > MAX_DUMP_BYTES ? STR(bot) : ""); return; } +#endif + msg_info("%s: Matched DANE %s at depth %d: %u %u %u %s%s%s", + TLScontext->namaddr, mspki ? + "TA public key verified certificate" : depth ? + "TA certificate" : "EE certificate", depth, u, s, m, + STR(top), dlen > MAX_DUMP_BYTES ? "..." : "", + dlen > MAX_DUMP_BYTES ? STR(bot) : ""); } #ifdef TEST diff --git a/src/tls/tls_fprint.c b/src/tls/tls_fprint.c index 39b5a52..dc3f99e 100644 --- a/src/tls/tls_fprint.c +++ b/src/tls/tls_fprint.c @@ -24,7 +24,7 @@ /* const char *mdalg; /* /* char *tls_pkey_fprint(peercert, mdalg) -/* X509 *peercert; +/* EVP_PKEY *peerpkey; /* const char *mdalg; /* DESCRIPTION /* tls_digest_byname() constructs, and optionally returns, an EVP_MD_CTX @@ -48,8 +48,6 @@ /* /* tls_pkey_fprint() returns a public-key fingerprint; in all /* other respects the function behaves as tls_cert_fprint(). -/* The var_tls_bc_pkey_fprint variable enables an incorrect -/* algorithm that was used in Postfix versions 2.9.[0-5]. /* The return value is dynamically allocated with mymalloc(), /* and the caller must eventually free it with myfree(). /* @@ -274,6 +272,9 @@ char *tls_serverid_digest(TLS_SESS_STATE *TLScontext, CHECK_OK_AND_DIGEST_CHARS(mdctx, props->protocols); CHECK_OK_AND_DIGEST_CHARS(mdctx, ciphers); + /* Just in case we make this destination-policy specific */ + CHECK_OK_AND_DIGEST_OBJECT(mdctx, &props->enable_rpk); + /* * Ensure separation of caches for sessions where DANE trust * configuration succeeded from those where it did not. The latter @@ -398,38 +399,24 @@ char *tls_cert_fprint(X509 *peercert, const char *mdalg) return (result); } -/* tls_pkey_fprint - extract public key fingerprint from certificate */ +/* tls_pkey_fprint - extract public key fingerprint */ -char *tls_pkey_fprint(X509 *peercert, const char *mdalg) +char *tls_pkey_fprint(EVP_PKEY *peerpkey, const char *mdalg) { - if (var_tls_bc_pkey_fprint) { - const char *myname = "tls_pkey_fprint"; - ASN1_BIT_STRING *key; - char *result; - - key = X509_get0_pubkey_bitstr(peercert); - if (key == 0) - msg_fatal("%s: error extracting legacy public-key fingerprint: %m", - myname); - - result = tls_data_fprint(key->data, key->length, mdalg); - return (result); - } else { - int len; - unsigned char *buf; - unsigned char *buf2; - char *result; - - len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(peercert), NULL); - buf2 = buf = mymalloc(len); - i2d_X509_PUBKEY(X509_get_X509_PUBKEY(peercert), &buf2); - if (buf2 - buf != len) - msg_panic("i2d_X509_PUBKEY invalid result length"); - - result = tls_data_fprint(buf, len, mdalg); - myfree(buf); - return (result); - } + int len; + unsigned char *buf; + unsigned char *buf2; + char *result; + + len = i2d_PUBKEY(peerpkey, NULL); + buf2 = buf = mymalloc(len); + i2d_PUBKEY(peerpkey, &buf2); + if (buf2 - buf != len) + msg_panic("i2d_PUBKEY invalid result length"); + + result = tls_data_fprint(buf, len, mdalg); + myfree(buf); + return (result); } #endif diff --git a/src/tls/tls_misc.c b/src/tls/tls_misc.c index b7acd1d..cf8f6fa 100644 --- a/src/tls/tls_misc.c +++ b/src/tls/tls_misc.c @@ -118,6 +118,14 @@ /* /* const EVP_MD *tls_validate_digest(dgst) /* const char *dgst; +/* +/* void tls_enable_client_rpk(ctx, ssl) +/* SSL_CTX *ctx; +/* SSL *ssl; +/* +/* void tls_enable_server_rpk(ctx, ssl) +/* SSL_CTX *ctx; +/* SSL *ssl; /* DESCRIPTION /* This module implements public and internal routines that /* support the TLS client and server. @@ -215,6 +223,12 @@ /* /* tls_validate_digest() returns a static handle for the named /* digest algorithm, or NULL on error. +/* +/* tls_enable_client_rpk() enables the use of raw public keys in the +/* client to server direction, if supported by the OpenSSL library. +/* +/* tls_enable_server_rpk() enables the use of raw public keys in the +/* server to client direction, if supported by the OpenSSL library. /* LICENSE /* .ad /* .fi @@ -762,7 +776,7 @@ int tls_library_init(void) /* * The default global config file is optional. With "default" - * initialisation we don't insist on a match for the requested + * initialization we don't insist on a match for the requested * application name, allowing fallback to the default application * name, even when a non-default application name is specified. * Errors in loading the default configuration are ignored. @@ -1028,7 +1042,6 @@ void tls_get_signature_params(TLS_SESS_STATE *TLScontext) SSL *ssl = TLScontext->con; int srvr = SSL_is_server(ssl); EVP_PKEY *dh_pkey = 0; - X509 *local_cert; EVP_PKEY *local_pkey = 0; X509 *peer_cert; EVP_PKEY *peer_pkey = 0; @@ -1060,18 +1073,23 @@ void tls_get_signature_params(TLS_SESS_STATE *TLScontext) } /* - * On the client end, the certificate may be preset, but not used, so we + * On the client end, the certificate may be present, but not used, so we * check via SSL_get_signature_nid(). This means that local signature * data on clients requires at least 1.1.1a. */ - if (srvr || SSL_get_signature_nid(ssl, &nid)) - local_cert = SSL_get_certificate(ssl); - else - local_cert = 0; - + if (srvr || SSL_get_signature_nid(ssl, &nid)) { + local_pkey = SSL_get_privatekey(ssl); + } /* Signature algorithms for the local end of the connection */ - if (local_cert) { - local_pkey = X509_get0_pubkey(local_cert); + if (local_pkey) { +#if OPENSSL_VERSION_PREREQ(3,2) + if (srvr) + TLScontext->stoc_rpk = TLSEXT_cert_type_rpk == + SSL_get_negotiated_server_cert_type(ssl); + else + TLScontext->ctos_rpk = TLSEXT_cert_type_rpk == + SSL_get_negotiated_client_cert_type(ssl); +#endif /* * Override the built-in name for the "ECDSA" algorithms OID, with @@ -1097,7 +1115,6 @@ void tls_get_signature_params(TLS_SESS_STATE *TLScontext) break; #endif } - /* No X509_free(local_cert) */ } /* @@ -1107,9 +1124,26 @@ void tls_get_signature_params(TLS_SESS_STATE *TLScontext) if (SSL_get_signature_nid(ssl, &nid) && nid != NID_undef) locl_sig_dgst = OBJ_nid2sn(nid); } - /* Signature algorithms for the peer end of the connection */ - if ((peer_cert = TLS_PEEK_PEER_CERT(ssl)) != 0) { + peer_cert = TLS_PEEK_PEER_CERT(ssl); + if (peer_cert != 0) { peer_pkey = X509_get0_pubkey(peer_cert); + } +#if OPENSSL_VERSION_PREREQ(3,2) + else { + peer_pkey = SSL_get0_peer_rpk(ssl); + } +#endif + + /* Signature algorithms for the peer end of the connection */ + if (peer_pkey != 0) { +#if OPENSSL_VERSION_PREREQ(3,2) + if (srvr) + TLScontext->ctos_rpk = TLSEXT_cert_type_rpk == + SSL_get_negotiated_client_cert_type(ssl); + else + TLScontext->stoc_rpk = TLSEXT_cert_type_rpk == + SSL_get_negotiated_server_cert_type(ssl); +#endif /* * Override the built-in name for the "ECDSA" algorithms OID, with @@ -1144,8 +1178,9 @@ void tls_get_signature_params(TLS_SESS_STATE *TLScontext) if (SSL_get_peer_signature_nid(ssl, &nid) && nid != NID_undef) peer_sig_dgst = OBJ_nid2sn(nid); - TLS_FREE_PEER_CERT(peer_cert); } + TLS_FREE_PEER_CERT(peer_cert); + if (kex_name) { TLScontext->kex_name = mystrdup(kex_name); TLScontext->kex_curve = kex_curve; @@ -1180,7 +1215,7 @@ void tls_log_summary(TLS_ROLE role, TLS_USAGE usage, TLS_SESS_STATE *ctx) */ vstring_sprintf(msg, "%s TLS connection %s %s %s%s%s: %s" " with cipher %s (%d/%d bits)", - !TLS_CERT_IS_PRESENT(ctx) ? "Anonymous" : + !TLS_CRED_IS_PRESENT(ctx) ? "Anonymous" : TLS_CERT_IS_SECURED(ctx) ? "Verified" : TLS_CERT_IS_TRUSTED(ctx) ? "Trusted" : "Untrusted", usage == TLS_USAGE_NEW ? "established" : "reused", @@ -1199,9 +1234,13 @@ void tls_log_summary(TLS_ROLE role, TLS_USAGE usage, TLS_SESS_STATE *ctx) vstring_sprintf_append(msg, " server-signature %s", ctx->srvr_sig_name); if (ctx->srvr_sig_curve && *ctx->srvr_sig_curve) - vstring_sprintf_append(msg, " (%s)", ctx->srvr_sig_curve); + vstring_sprintf_append(msg, " (%s%s)", ctx->srvr_sig_curve, + ctx->stoc_rpk ? " raw public key" : ""); else if (ctx->srvr_sig_bits > 0) - vstring_sprintf_append(msg, " (%d bits)", ctx->srvr_sig_bits); + vstring_sprintf_append(msg, " (%d bit%s)", ctx->srvr_sig_bits, + ctx->stoc_rpk ? " raw public key" : "s"); + else if (ctx->stoc_rpk) + vstring_sprintf_append(msg, " (raw public key)"); if (ctx->srvr_sig_dgst && *ctx->srvr_sig_dgst) vstring_sprintf_append(msg, " server-digest %s", ctx->srvr_sig_dgst); @@ -1210,9 +1249,13 @@ void tls_log_summary(TLS_ROLE role, TLS_USAGE usage, TLS_SESS_STATE *ctx) vstring_sprintf_append(msg, " client-signature %s", ctx->clnt_sig_name); if (ctx->clnt_sig_curve && *ctx->clnt_sig_curve) - vstring_sprintf_append(msg, " (%s)", ctx->clnt_sig_curve); + vstring_sprintf_append(msg, " (%s%s)", ctx->clnt_sig_curve, + ctx->ctos_rpk ? " raw public key" : ""); else if (ctx->clnt_sig_bits > 0) - vstring_sprintf_append(msg, " (%d bits)", ctx->clnt_sig_bits); + vstring_sprintf_append(msg, " (%d bit%s)", ctx->clnt_sig_bits, + ctx->ctos_rpk ? " raw public key" : "s"); + else if (ctx->ctos_rpk) + vstring_sprintf_append(msg, " (raw public key)"); if (ctx->clnt_sig_dgst && *ctx->clnt_sig_dgst) vstring_sprintf_append(msg, " client-digest %s", ctx->clnt_sig_dgst); @@ -1288,6 +1331,8 @@ TLS_SESS_STATE *tls_alloc_sess_context(int log_mask, const char *namaddr) TLScontext->cipher_name = 0; TLScontext->kex_name = 0; TLScontext->kex_curve = 0; + TLScontext->ctos_rpk = 0; + TLScontext->stoc_rpk = 0; TLScontext->clnt_sig_name = 0; TLScontext->clnt_sig_curve = 0; TLScontext->clnt_sig_dgst = 0; @@ -1702,6 +1747,52 @@ const EVP_MD *tls_validate_digest(const char *dgst) return md_alg; } +void tls_enable_client_rpk(SSL_CTX *ctx, SSL *ssl) +{ +#if OPENSSL_VERSION_PREREQ(3,2) + static int warned = 0; + static const unsigned char cert_types_rpk[] = { + TLSEXT_cert_type_rpk, + TLSEXT_cert_type_x509 + }; + + if ((ctx && !SSL_CTX_set1_client_cert_type(ctx, cert_types_rpk, + sizeof(cert_types_rpk))) || + (ssl && !SSL_set1_client_cert_type(ssl, cert_types_rpk, + sizeof(cert_types_rpk)))) { + if (warned++) { + ERR_clear_error(); + return; + } + msg_warn("Failed to enable client to server raw public key support"); + tls_print_errors(); + } +#endif +} + +void tls_enable_server_rpk(SSL_CTX *ctx, SSL *ssl) +{ +#if OPENSSL_VERSION_PREREQ(3,2) + static int warned = 0; + static const unsigned char cert_types_rpk[] = { + TLSEXT_cert_type_rpk, + TLSEXT_cert_type_x509 + }; + + if ((ctx && !SSL_CTX_set1_server_cert_type(ctx, cert_types_rpk, + sizeof(cert_types_rpk))) || + (ssl && !SSL_set1_server_cert_type(ssl, cert_types_rpk, + sizeof(cert_types_rpk)))) { + if (warned++) { + ERR_clear_error(); + return; + } + msg_warn("Failed to enable server to client raw public key support"); + tls_print_errors(); + } +#endif +} + #else /* diff --git a/src/tls/tls_proxy.h b/src/tls/tls_proxy.h index ca664c6..6528639 100644 --- a/src/tls/tls_proxy.h +++ b/src/tls/tls_proxy.h @@ -32,8 +32,10 @@ #ifdef USE_TLS /* - * TLS_CLIENT_PARAMS structure. If this changes, update all - * TLS_CLIENT_PARAMS related functions in tls_proxy_client_*.c. + * TLS_CLIENT_PARAMS structure, to communicate global TLS library settings + * that are the same for all TLS client contexts. This information is used + * in tlsproxy(8) to detect inconsistencies. If this structure is changed, + * update all TLS_CLIENT_PARAMS related functions in tls_proxy_client_*.c. * * In the serialization these attributes are identified by their configuration * parameter names. @@ -106,11 +108,11 @@ extern VSTREAM *tls_proxy_open(const char *, int, VSTREAM *, const char *, ((props)->a12), ((props)->a13), ((props)->a14)) #define TLS_PROXY_CLIENT_START_PROPS(props, a1, a2, a3, a4, a5, a6, a7, a8, \ - a9, a10, a11, a12, a13, a14) \ + a9, a10, a11, a12, a13, a14, a15) \ (((props)->a1), ((props)->a2), ((props)->a3), \ ((props)->a4), ((props)->a5), ((props)->a6), ((props)->a7), \ ((props)->a8), ((props)->a9), ((props)->a10), ((props)->a11), \ - ((props)->a12), ((props)->a13), ((props)->a14)) + ((props)->a12), ((props)->a13), ((props)->a14), ((props)->a15)) extern TLS_SESS_STATE *tls_proxy_context_receive(VSTREAM *); extern void tls_proxy_context_free(TLS_SESS_STATE *); @@ -168,6 +170,8 @@ extern void tls_proxy_server_start_free(TLS_SERVER_START_PROPS *); #define TLS_ATTR_KEX_NAME "key_exchange" #define TLS_ATTR_KEX_CURVE "key_exchange_curve" #define TLS_ATTR_KEX_BITS "key_exchange_bits" +#define TLS_ATTR_CTOS_RPK "ctos_rpk" +#define TLS_ATTR_STOC_RPK "stoc_rpk" #define TLS_ATTR_CLNT_SIG_NAME "clnt_signature" #define TLS_ATTR_CLNT_SIG_CURVE "clnt_signature_curve" #define TLS_ATTR_CLNT_SIG_BITS "clnt_signature_bits" @@ -237,6 +241,7 @@ extern void tls_proxy_server_start_free(TLS_SERVER_START_PROPS *); * TLS_CLIENT_START_PROPS attributes. */ #define TLS_ATTR_TIMEOUT "timeout" +#define TLS_ATTR_ENABLE_RPK "enable_rpk" #define TLS_ATTR_TLS_LEVEL "tls_level" #define TLS_ATTR_NEXTHOP "nexthop" #define TLS_ATTR_HOST "host" diff --git a/src/tls/tls_proxy_client_print.c b/src/tls/tls_proxy_client_print.c index 1cc5778..81e50b9 100644 --- a/src/tls/tls_proxy_client_print.c +++ b/src/tls/tls_proxy_client_print.c @@ -257,6 +257,7 @@ int tls_proxy_client_start_print(ATTR_PRINT_COMMON_FN print_fn, ret = print_fn(fp, flags | ATTR_FLAG_MORE, SEND_ATTR_INT(TLS_ATTR_TIMEOUT, props->timeout), + SEND_ATTR_INT(TLS_ATTR_ENABLE_RPK, props->enable_rpk), SEND_ATTR_INT(TLS_ATTR_TLS_LEVEL, props->tls_level), SEND_ATTR_STR(TLS_ATTR_NEXTHOP, STRING_OR_EMPTY(props->nexthop)), diff --git a/src/tls/tls_proxy_client_scan.c b/src/tls/tls_proxy_client_scan.c index a69388c..d36cf4d 100644 --- a/src/tls/tls_proxy_client_scan.c +++ b/src/tls/tls_proxy_client_scan.c @@ -451,6 +451,7 @@ int tls_proxy_client_start_scan(ATTR_SCAN_COMMON_FN scan_fn, VSTREAM *fp, props->dane = 0; /* scan_fn may return early */ ret = scan_fn(fp, flags | ATTR_FLAG_MORE, RECV_ATTR_INT(TLS_ATTR_TIMEOUT, &props->timeout), + RECV_ATTR_INT(TLS_ATTR_ENABLE_RPK, &props->enable_rpk), RECV_ATTR_INT(TLS_ATTR_TLS_LEVEL, &props->tls_level), RECV_ATTR_STR(TLS_ATTR_NEXTHOP, nexthop), RECV_ATTR_STR(TLS_ATTR_HOST, host), @@ -478,7 +479,7 @@ int tls_proxy_client_start_scan(ATTR_SCAN_COMMON_FN scan_fn, VSTREAM *fp, props->cipher_grade = vstring_export(cipher_grade); props->cipher_exclusions = vstring_export(cipher_exclusions); props->mdalg = vstring_export(mdalg); - ret = (ret == 14 ? 1 : -1); + ret = (ret == 15 ? 1 : -1); if (ret != 1) { tls_proxy_client_start_free(props); props = 0; diff --git a/src/tls/tls_proxy_context_print.c b/src/tls/tls_proxy_context_print.c index 04123cb..930410a 100644 --- a/src/tls/tls_proxy_context_print.c +++ b/src/tls/tls_proxy_context_print.c @@ -88,6 +88,10 @@ int tls_proxy_context_print(ATTR_PRINT_COMMON_FN print_fn, VSTREAM *fp, STRING_OR_EMPTY(tp->kex_curve)), SEND_ATTR_INT(TLS_ATTR_KEX_BITS, tp->kex_bits), + SEND_ATTR_INT(TLS_ATTR_CTOS_RPK, + tp->ctos_rpk), + SEND_ATTR_INT(TLS_ATTR_STOC_RPK, + tp->stoc_rpk), SEND_ATTR_STR(TLS_ATTR_CLNT_SIG_NAME, STRING_OR_EMPTY(tp->clnt_sig_name)), SEND_ATTR_STR(TLS_ATTR_CLNT_SIG_CURVE, diff --git a/src/tls/tls_proxy_context_scan.c b/src/tls/tls_proxy_context_scan.c index 1d463ad..48aaff6 100644 --- a/src/tls/tls_proxy_context_scan.c +++ b/src/tls/tls_proxy_context_scan.c @@ -113,6 +113,8 @@ int tls_proxy_context_scan(ATTR_SCAN_COMMON_FN scan_fn, VSTREAM *fp, RECV_ATTR_STR(TLS_ATTR_KEX_NAME, kex_name), RECV_ATTR_STR(TLS_ATTR_KEX_CURVE, kex_curve), RECV_ATTR_INT(TLS_ATTR_KEX_BITS, &tls_context->kex_bits), + RECV_ATTR_INT(TLS_ATTR_CTOS_RPK, &tls_context->ctos_rpk), + RECV_ATTR_INT(TLS_ATTR_STOC_RPK, &tls_context->stoc_rpk), RECV_ATTR_STR(TLS_ATTR_CLNT_SIG_NAME, clnt_sig_name), RECV_ATTR_STR(TLS_ATTR_CLNT_SIG_CURVE, clnt_sig_curve), RECV_ATTR_INT(TLS_ATTR_CLNT_SIG_BITS, &tls_context->clnt_sig_bits), @@ -139,7 +141,7 @@ int tls_proxy_context_scan(ATTR_SCAN_COMMON_FN scan_fn, VSTREAM *fp, tls_context->srvr_sig_curve = vstring_export(srvr_sig_curve); tls_context->srvr_sig_dgst = vstring_export(srvr_sig_dgst); tls_context->namaddr = vstring_export(namaddr); - ret = (ret == 22 ? 1 : -1); + ret = (ret == 24 ? 1 : -1); if (ret != 1) { tls_proxy_context_free(tls_context); tls_context = 0; diff --git a/src/tls/tls_server.c b/src/tls/tls_server.c index 262cda9..88b3326 100644 --- a/src/tls/tls_server.c +++ b/src/tls/tls_server.c @@ -62,8 +62,8 @@ /* available as: /* .IP TLScontext->peer_status /* A bitmask field that records the status of the peer certificate -/* verification. One or more of TLS_CERT_FLAG_PRESENT and -/* TLS_CERT_FLAG_TRUSTED. +/* verification. One or more of TLS_CRED_FLAG_CERT, TLS_CRED_FLAG_RPK +/* and TLS_CERT_FLAG_TRUSTED. /* .IP TLScontext->peer_CN /* Extracted CommonName of the peer, or zero-length string /* when information could not be extracted. @@ -637,6 +637,13 @@ TLS_APPL_STATE *tls_server_init(const TLS_SERVER_INIT_PROPS *props) } /* + * Always support server->client raw public keys, if they're good enough + * for the client, they're good enough for us. + */ + tls_enable_server_rpk(server_ctx, NULL); + tls_enable_server_rpk(sni_ctx, NULL); + + /* * Upref and share the cert store. Sadly we can't yet use * SSL_CTX_set1_cert_store(3) which was added in OpenSSL 1.1.0. */ @@ -865,11 +872,19 @@ TLS_SESS_STATE *tls_server_start(const TLS_SERVER_START_PROPS *props) tls_free_context(TLScontext); return (0); } -#ifdef SSL_SECOP_PEER - /* When authenticating the peer, use 80-bit plus OpenSSL security level */ + + /* + * When encryption is mandatory use the 80-bit plus OpenSSL security level. + */ if (props->requirecert) SSL_set_security_level(TLScontext->con, 1); -#endif + + /* + * Also enable client->server raw public keys, provided we're not + * interested in client certificate fingerprints. + */ + if (props->enable_rpk) + tls_enable_client_rpk(NULL, TLScontext->con); /* * Before really starting anything, try to seed the PRNG a little bit @@ -946,6 +961,7 @@ TLS_SESS_STATE *tls_server_post_accept(TLS_SESS_STATE *TLScontext) { const SSL_CIPHER *cipher; X509 *peer; + EVP_PKEY *pkey = 0; char buf[CCERT_BUFSIZ]; /* Turn off packet dump if only dumping the handshake */ @@ -966,8 +982,17 @@ TLS_SESS_STATE *tls_server_post_accept(TLS_SESS_STATE *TLScontext) * actual information. We want to save it for later use. */ peer = TLS_PEEK_PEER_CERT(TLScontext->con); + if (peer) { + pkey = X509_get0_pubkey(peer); + } +#if OPENSSL_VERSION_PREREQ(3,2) + else { + pkey = SSL_get0_peer_rpk(TLScontext->con); + } +#endif + if (peer != NULL) { - TLScontext->peer_status |= TLS_CERT_FLAG_PRESENT; + TLScontext->peer_status |= TLS_CRED_FLAG_CERT; if (SSL_get_verify_result(TLScontext->con) == X509_V_OK) TLScontext->peer_status |= TLS_CERT_FLAG_TRUSTED; @@ -981,16 +1006,23 @@ TLS_SESS_STATE *tls_server_post_accept(TLS_SESS_STATE *TLScontext) } TLScontext->peer_CN = tls_peer_CN(peer, TLScontext); TLScontext->issuer_CN = tls_issuer_CN(peer, TLScontext); - TLScontext->peer_cert_fprint = tls_cert_fprint(peer, TLScontext->mdalg); - TLScontext->peer_pkey_fprint = tls_pkey_fprint(peer, TLScontext->mdalg); + TLScontext->peer_cert_fprint = + tls_cert_fprint(peer, TLScontext->mdalg); + TLScontext->peer_pkey_fprint = + tls_pkey_fprint(pkey, TLScontext->mdalg); if (TLScontext->log_mask & (TLS_LOG_VERBOSE | TLS_LOG_PEERCERT)) { - msg_info("%s: subject_CN=%s, issuer=%s, fingerprint=%s" - ", pkey_fingerprint=%s", + msg_info("%s: subject_CN=%s, issuer=%s%s%s%s%s", TLScontext->namaddr, TLScontext->peer_CN, TLScontext->issuer_CN, - TLScontext->peer_cert_fprint, - TLScontext->peer_pkey_fprint); + *TLScontext->peer_cert_fprint ? + ", cert fingerprint=" : "", + *TLScontext->peer_cert_fprint ? + TLScontext->peer_cert_fprint : "", + *TLScontext->peer_pkey_fprint ? + ", pkey fingerprint=" : "", + *TLScontext->peer_pkey_fprint ? + TLScontext->peer_pkey_fprint : ""); } TLS_FREE_PEER_CERT(peer); @@ -1013,7 +1045,22 @@ TLS_SESS_STATE *tls_server_post_accept(TLS_SESS_STATE *TLScontext) TLScontext->peer_CN = mystrdup(""); TLScontext->issuer_CN = mystrdup(""); TLScontext->peer_cert_fprint = mystrdup(""); - TLScontext->peer_pkey_fprint = mystrdup(""); + if (!pkey) { + TLScontext->peer_pkey_fprint = mystrdup(""); + } else { + + /* + * Raw public keys don't involve CA trust, and we don't have a + * way to associate DANE TLSA RRs with clients just yet, we just + * make the fingerprint available to the access(5) layer. + */ + TLScontext->peer_status |= TLS_CRED_FLAG_RPK; + TLScontext->peer_pkey_fprint = + tls_pkey_fprint(pkey, TLScontext->mdalg); + if (TLScontext->log_mask & (TLS_LOG_VERBOSE | TLS_LOG_PEERCERT)) + msg_info("%s: raw public key fingerprint=%s", + TLScontext->namaddr, TLScontext->peer_pkey_fprint); + } } /* diff --git a/src/tls/tls_verify.c b/src/tls/tls_verify.c index f32f32b..c643f18 100644 --- a/src/tls/tls_verify.c +++ b/src/tls/tls_verify.c @@ -144,6 +144,7 @@ int tls_verify_certificate_callback(int ok, X509_STORE_CTX *ctx) int depth; SSL *con; TLS_SESS_STATE *TLScontext; + EVP_PKEY *rpk = 0; /* May be NULL as of OpenSSL 1.0, thanks for the API change! */ cert = X509_STORE_CTX_get_current_cert(ctx); @@ -151,6 +152,10 @@ int tls_verify_certificate_callback(int ok, X509_STORE_CTX *ctx) con = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); TLScontext = SSL_get_ex_data(con, TLScontext_index); depth = X509_STORE_CTX_get_error_depth(ctx); +#if OPENSSL_VERSION_PREREQ(3,2) + if (cert == 0) + rpk = X509_STORE_CTX_get0_rpk(ctx); +#endif /* * Transient failures to load the (DNS or synthetic TLSA) trust settings @@ -174,12 +179,15 @@ int tls_verify_certificate_callback(int ok, X509_STORE_CTX *ctx) update_error_state(TLScontext, depth, cert, err); if (TLScontext->log_mask & TLS_LOG_VERBOSE) { - if (cert) + if (cert) { X509_NAME_oneline(X509_get_subject_name(cert), buf, sizeof(buf)); - else - strcpy(buf, "<unknown>"); - msg_info("%s: depth=%d verify=%d subject=%s", - TLScontext->namaddr, depth, ok, printable(buf, '?')); + msg_info("%s: depth=%d verify=%d subject=%s", + TLScontext->namaddr, depth, ok, printable(buf, '?')); + } else if (rpk) { + msg_info("%s: verify=%d raw public key", TLScontext->namaddr, ok); + } else { + msg_info("%s: depth=%d verify=%d", TLScontext->namaddr, depth, ok); + } } return (1); } diff --git a/src/tlsproxy/tlsproxy.c b/src/tlsproxy/tlsproxy.c index 7c0d814..0ebf52c 100644 --- a/src/tlsproxy/tlsproxy.c +++ b/src/tlsproxy/tlsproxy.c @@ -237,6 +237,12 @@ /* .IP "\fBtlsproxy_tls_chain_files ($smtpd_tls_chain_files)\fR" /* Files with the Postfix \fBtlsproxy\fR(8) server keys and certificate /* chains in PEM format. +/* .PP +/* Available in Postfix version 3.9 and later: +/* .IP "\fBtlsproxy_tls_enable_rpk ($smtpd_tls_enable_rpk)\fR" +/* Request that remote SMTP clients send an RFC7250 raw public key +/* instead of an X.509 certificate, when asking or requiring client +/* authentication. /* STARTTLS CLIENT CONTROLS /* .ad /* .fi @@ -436,6 +442,7 @@ bool var_smtpd_use_tls; bool var_smtpd_enforce_tls; bool var_smtpd_tls_ask_ccert; bool var_smtpd_tls_req_ccert; +bool var_smtpd_tls_enable_rpk; bool var_smtpd_tls_set_sessid; char *var_smtpd_relay_ccerts; char *var_smtpd_tls_chain_files; @@ -465,6 +472,7 @@ bool var_tlsp_use_tls; bool var_tlsp_enforce_tls; bool var_tlsp_tls_ask_ccert; bool var_tlsp_tls_req_ccert; +bool var_tlsp_tls_enable_rpk; bool var_tlsp_tls_set_sessid; char *var_tlsp_tls_chain_files; char *var_tlsp_tls_cert_file; @@ -1081,6 +1089,7 @@ static int tlsp_server_start_pre_handshake(TLSP_STATE *state) timeout = 0, /* unused */ requirecert = (var_tlsp_tls_req_ccert && var_tlsp_enforce_tls), + enable_rpk = var_tlsp_tls_enable_rpk, serverid = state->server_id, namaddr = state->remote_endpt, cipher_grade = cipher_grade, @@ -1827,6 +1836,7 @@ int main(int argc, char **argv) VAR_SMTPD_ENFORCE_TLS, DEF_SMTPD_ENFORCE_TLS, &var_smtpd_enforce_tls, VAR_SMTPD_TLS_ACERT, DEF_SMTPD_TLS_ACERT, &var_smtpd_tls_ask_ccert, VAR_SMTPD_TLS_RCERT, DEF_SMTPD_TLS_RCERT, &var_smtpd_tls_req_ccert, + VAR_SMTPD_TLS_ENABLE_RPK, DEF_SMTPD_TLS_ENABLE_RPK, &var_smtpd_tls_enable_rpk, VAR_SMTPD_TLS_SET_SESSID, DEF_SMTPD_TLS_SET_SESSID, &var_smtpd_tls_set_sessid, VAR_SMTP_USE_TLS, DEF_SMTP_USE_TLS, &var_smtp_use_tls, VAR_SMTP_ENFORCE_TLS, DEF_SMTP_ENFORCE_TLS, &var_smtp_enforce_tls, @@ -1837,6 +1847,7 @@ int main(int argc, char **argv) VAR_TLSP_ENFORCE_TLS, DEF_TLSP_ENFORCE_TLS, &var_tlsp_enforce_tls, VAR_TLSP_TLS_ACERT, DEF_TLSP_TLS_ACERT, &var_tlsp_tls_ask_ccert, VAR_TLSP_TLS_RCERT, DEF_TLSP_TLS_RCERT, &var_tlsp_tls_req_ccert, + VAR_TLSP_TLS_ENABLE_RPK, DEF_TLSP_TLS_ENABLE_RPK, &var_tlsp_tls_enable_rpk, VAR_TLSP_TLS_SET_SESSID, DEF_TLSP_TLS_SET_SESSID, &var_tlsp_tls_set_sessid, VAR_TLSP_CLNT_USE_TLS, DEF_TLSP_CLNT_USE_TLS, &var_tlsp_clnt_use_tls, VAR_TLSP_CLNT_ENFORCE_TLS, DEF_TLSP_CLNT_ENFORCE_TLS, &var_tlsp_clnt_enforce_tls, diff --git a/src/trivial-rewrite/resolve.c b/src/trivial-rewrite/resolve.c index 40e6aa5..df761e7 100644 --- a/src/trivial-rewrite/resolve.c +++ b/src/trivial-rewrite/resolve.c @@ -427,7 +427,7 @@ static void resolve_addr(RES_CONTEXT *rp, char *sender, char *addr, if (!valid_mailhost_literal(rcpt_domain, DONT_GRIPE)) *flags |= RESOLVE_FLAG_ERROR; } else if (var_smtputf8_enable - && valid_utf8_string(STR(nextrcpt), LEN(nextrcpt)) == 0) { + && valid_utf8_stringz(STR(nextrcpt)) == 0) { *flags |= RESOLVE_FLAG_ERROR; } else if (!valid_utf8_hostname(var_smtputf8_enable, rcpt_domain, DONT_GRIPE)) { diff --git a/src/trivial-rewrite/trivial-rewrite.c b/src/trivial-rewrite/trivial-rewrite.c index 675af80..bb8da09 100644 --- a/src/trivial-rewrite/trivial-rewrite.c +++ b/src/trivial-rewrite/trivial-rewrite.c @@ -122,9 +122,10 @@ /* .PP /* Available in Postfix 2.2 and later: /* .IP "\fBremote_header_rewrite_domain (empty)\fR" -/* Don't rewrite message headers from remote clients at all when -/* this parameter is empty; otherwise, rewrite message headers and -/* append the specified domain name to incomplete addresses. +/* Rewrite or add message headers in mail from remote clients if +/* the remote_header_rewrite_domain parameter value is non-empty, +/* updating incomplete addresses with the domain specified in the +/* remote_header_rewrite_domain parameter, and adding missing headers. /* ROUTING CONTROLS /* .ad /* .fi @@ -141,10 +142,12 @@ /* final delivery to domains listed with $virtual_mailbox_domains. /* .IP "\fBrelay_transport (relay)\fR" /* The default mail delivery transport and next-hop destination for -/* remote delivery to domains listed with $relay_domains. +/* the relay domain address class: recipient domains that match +/* $relay_domains. /* .IP "\fBdefault_transport (smtp)\fR" /* The default mail delivery transport and next-hop destination for -/* destinations that do not match $mydestination, $inet_interfaces, +/* the default domain class: recipient domains that do not match +/* $mydestination, $inet_interfaces, /* $proxy_interfaces, $virtual_alias_domains, $virtual_mailbox_domains, /* or $relay_domains. /* .IP "\fBparent_domain_matches_subdomains (see 'postconf -d' output)\fR" @@ -152,8 +155,8 @@ /* matches subdomains of example.com, /* instead of requiring an explicit ".example.com" pattern. /* .IP "\fBrelayhost (empty)\fR" -/* The next-hop destination(s) for non-local mail; overrides non-local -/* domains in recipient addresses. +/* The next-hop destination(s) for non-local mail; takes precedence +/* over non-local domains in recipient addresses. /* .IP "\fBtransport_maps (empty)\fR" /* Optional lookup tables with mappings from recipient address to /* (message delivery transport, next-hop destination). diff --git a/src/util/Makefile.in b/src/util/Makefile.in index f69dec5..01211fb 100644 --- a/src/util/Makefile.in +++ b/src/util/Makefile.in @@ -45,7 +45,7 @@ SRCS = alldig.c allprint.c argv.c argv_split.c attr_clnt.c attr_print0.c \ byte_mask.c known_tcp_ports.c argv_split_at.c dict_stream.c \ sane_strtol.c hash_fnv.c ldseed.c mkmap_cdb.c mkmap_db.c mkmap_dbm.c \ mkmap_fail.c mkmap_lmdb.c mkmap_open.c mkmap_sdbm.c inet_prefix_top.c \ - inet_addr_sizes.c + inet_addr_sizes.c quote_for_json.c OBJS = alldig.o allprint.o argv.o argv_split.o attr_clnt.o attr_print0.o \ attr_print64.o attr_print_plain.o attr_scan0.o attr_scan64.o \ attr_scan_plain.o auto_clnt.o base64_code.o basename.o binhash.o \ @@ -91,7 +91,8 @@ OBJS = alldig.o allprint.o argv.o argv_split.o attr_clnt.o attr_print0.o \ msg_logger.o logwriter.o unix_dgram_connect.o unix_dgram_listen.o \ byte_mask.o known_tcp_ports.o argv_split_at.o dict_stream.o \ sane_strtol.o hash_fnv.o ldseed.o mkmap_db.o mkmap_dbm.o \ - mkmap_fail.o mkmap_open.o inet_prefix_top.o inet_addr_sizes.o + mkmap_fail.o mkmap_open.o inet_prefix_top.o inet_addr_sizes.o \ + quote_for_json.o # MAP_OBJ is for maps that may be dynamically loaded with dynamicmaps.cf. # When hard-linking these, makedefs sets NON_PLUGIN_MAP_OBJ=$(MAP_OBJ), # otherwise it sets the PLUGIN_* macros. @@ -145,7 +146,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 + clean_env inet_prefix_top printable readlline quote_for_json 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 @@ -365,6 +366,16 @@ unescape: $(LIB) $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(SYSLIBS) mv junk $@.o +printable: $(LIB) + mv $@.o junk + $(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) @@ -609,6 +620,11 @@ inet_prefix_top: $(LIB) $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(SYSLIBS) mv junk $@.o +quote_for_json: $(LIB) + mv $@.o junk + $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(SYSLIBS) + mv junk $@.o + tests: all valid_hostname_test mac_expand_test dict_test unescape_test \ hex_quote_test ctable_test inet_addr_list_test base64_code_test \ attr_scan64_test attr_scan0_test host_port_test dict_tests \ @@ -618,7 +634,8 @@ tests: all valid_hostname_test mac_expand_test dict_test unescape_test \ strcasecmp_utf8_test vbuf_print_test miss_endif_cidr_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 + binhash_test argv_test inet_prefix_top_test printable_test \ + valid_utf8_string_test readlline_test quote_for_json_test dict_tests: all dict_test \ dict_pcre_tests dict_cidr_test dict_thash_test dict_static_test \ @@ -650,6 +667,15 @@ unescape_test: unescape unescape.in unescape.ref # diff unescape.in unescape.tmp rm -f unescape.tmp +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 + hex_quote_test: hex_quote $(SHLIB_ENV) ${VALGRIND} ./hex_quote <hex_quote.c | od -cb >hex_quote.tmp od -cb <hex_quote.c >hex_quote.ref @@ -1083,6 +1109,9 @@ argv_test: argv inet_prefix_top_test: inet_prefix_top $(SHLIB_ENV) ${VALGRIND} ./inet_prefix_top +quote_for_json_test: quote_for_json + $(SHLIB_ENV) ${VALGRIND} ./quote_for_json + depend: $(MAKES) (sed '1,/^# do not edit/!d' Makefile.in; \ set -e; for i in [a-z][a-z0-9]*.c; do \ @@ -1119,9 +1148,12 @@ allspace.o: vbuf.h allspace.o: vstring.h argv.o: argv.c argv.o: argv.h +argv.o: check_arg.h argv.o: msg.h argv.o: mymalloc.h argv.o: sys_defs.h +argv.o: vbuf.h +argv.o: vstring.h argv_attr_print.o: argv.h argv_attr_print.o: argv_attr.h argv_attr_print.o: argv_attr_print.c @@ -2157,6 +2189,7 @@ logwriter.o: logwriter.c logwriter.o: logwriter.h logwriter.o: msg.h logwriter.o: mymalloc.h +logwriter.o: name_code.h logwriter.o: safe_open.h logwriter.o: sys_defs.h logwriter.o: vbuf.h @@ -2525,11 +2558,16 @@ posix_signals.o: posix_signals.c posix_signals.o: posix_signals.h posix_signals.o: sys_defs.h printable.o: check_arg.h +printable.o: parse_utf8_char.h printable.o: printable.c printable.o: stringops.h printable.o: sys_defs.h printable.o: vbuf.h printable.o: vstring.h +quote_for_json.o: quote_for_json.c +quote_for_json.o: stringops.h +quote_for_json.o: sys_defs.h +quote_for_json.o: vstring.h rand_sleep.o: iostuff.h rand_sleep.o: msg.h rand_sleep.o: myrand.h @@ -2848,6 +2886,7 @@ valid_utf8_hostname.o: valid_utf8_hostname.h valid_utf8_hostname.o: vbuf.h valid_utf8_hostname.o: vstring.h valid_utf8_string.o: check_arg.h +valid_utf8_string.o: parse_utf8_char.h valid_utf8_string.o: stringops.h valid_utf8_string.o: sys_defs.h valid_utf8_string.o: valid_utf8_string.c diff --git a/src/util/argv.c b/src/util/argv.c index 4e05fd0..332426e 100644 --- a/src/util/argv.c +++ b/src/util/argv.c @@ -53,6 +53,11 @@ /* ssize_t pos; /* ssize_t how_many; /* +/* char *argv_join(buf, argvp, delim) +/* VSTRING *buf; +/* ARGV *argvp; +/* int delim; +/* /* void ARGV_FAKE_BEGIN(argv, arg) /* const char *arg; /* @@ -109,6 +114,10 @@ /* starting at the specified array position. The result is /* null-terminated. /* +/* argv_join() joins all elements in an array using the +/* specified delimiter value, and appends the result to the +/* specified buffer. +/* /* ARGV_FAKE_BEGIN/END are an optimization for the case where /* a single string needs to be passed into an ARGV-based /* interface. ARGV_FAKE_BEGIN() opens a statement block and @@ -148,6 +157,7 @@ #include "mymalloc.h" #include "msg.h" +#include "vstring.h" #include "argv.h" #ifdef TEST @@ -379,6 +389,20 @@ void argv_delete(ARGV *argvp, ssize_t first, ssize_t how_many) argvp->argc -= how_many; } +/* argv_join - concatenate array elements with delimiter */ + +char *argv_join(VSTRING *buf, ARGV *argv, int delim) +{ + char **cpp; + + for (cpp = argv->argv; *cpp; cpp++) { + vstring_strcat(buf, *cpp); + if (cpp[1]) + VSTRING_ADDCH(buf, delim); + } + return (vstring_str(buf)); +} + #ifdef TEST /* @@ -402,6 +426,7 @@ typedef struct TEST_CASE { const char *exp_panic_msg; /* expected panic */ int exp_argc; /* expected array length */ const char *exp_argv[ARRAY_LEN]; /* expected array content */ + int join_delim; /* argv_join() delimiter */ } TEST_CASE; #define TERMINATE_ARRAY (1) @@ -559,6 +584,24 @@ static ARGV *test_argv_bad_delete3(const TEST_CASE *tp, ARGV *argvp) return (argvp); } +/* test_argv_join - populate, join, and overwrite */ + +static ARGV *test_argv_join(const TEST_CASE *tp, ARGV *argvp) +{ + VSTRING *buf = vstring_alloc(100); + + /* + * Impedance mismatch: argv_join() produces output to VSTRING, but the + * test fixture wants output to ARGV. + */ + test_argv_populate(tp, argvp); + argv_join(buf, argvp, tp->join_delim); + argv_delete(argvp, 0, argvp->argc); + argv_add(argvp, vstring_str(buf), ARGV_END); + vstring_free(buf); + return (argvp); +} + /* test_argv_verify - verify result */ static int test_argv_verify(const TEST_CASE *tp, ARGV *argvp) @@ -573,7 +616,7 @@ static int test_argv_verify(const TEST_CASE *tp, ARGV *argvp) } if (strcmp(vstring_str(test_panic_str), tp->exp_panic_msg) != 0) { msg_warn("test case '%s': got '%s', want: '%s'", - tp->label, vstring_str(test_panic_str), tp->exp_panic_msg); + tp->label, vstring_str(test_panic_str), tp->exp_panic_msg); return (FAIL); } return (PASS); @@ -682,6 +725,18 @@ static const TEST_CASE test_cases[] = { {"foo", "baz", "bar", 0}, 0, test_argv_bad_delete3, "argv_delete bad range: (start=100 count=1)" }, + {"argv_join, multiple strings", + {"foo", "baz", "bar", 0}, 0, test_argv_join, + 0, 1, {"foo:baz:bar", 0}, ':' + }, + {"argv_join, one string", + {"foo", 0}, 0, test_argv_join, + 0, 1, {"foo", 0}, ':' + }, + {"argv_join, empty", + {0}, 0, test_argv_join, + 0, 1, {"", 0}, ':' + }, 0, }; diff --git a/src/util/argv.h b/src/util/argv.h index b0098ce..f1e746a 100644 --- a/src/util/argv.h +++ b/src/util/argv.h @@ -33,6 +33,8 @@ extern void argv_truncate(ARGV *, ssize_t); extern void argv_insert_one(ARGV *, ssize_t, const char *); extern void argv_replace_one(ARGV *, ssize_t, const char *); extern void argv_delete(ARGV *, ssize_t, ssize_t); +struct VSTRING; +extern char *argv_join(struct VSTRING *buf, ARGV *, int); extern ARGV *argv_free(ARGV *); extern ARGV *argv_split(const char *, const char *); diff --git a/src/util/casefold.c b/src/util/casefold.c index d3ebd4b..94860b8 100644 --- a/src/util/casefold.c +++ b/src/util/casefold.c @@ -300,7 +300,7 @@ int main(int argc, char **argv) encode_utf8(buffer, codepoint); if (msg_verbose) vstream_printf("U+%X -> %s\n", codepoint, STR(buffer)); - if (valid_utf8_string(STR(buffer), LEN(buffer)) == 0) + if (valid_utf8_stringz(STR(buffer)) == 0) msg_fatal("bad utf-8 encoding for U+%X\n", codepoint); casefold(dest, STR(buffer)); } diff --git a/src/util/dict_inline.c b/src/util/dict_inline.c index 72339b2..d7f9344 100644 --- a/src/util/dict_inline.c +++ b/src/util/dict_inline.c @@ -87,7 +87,7 @@ DICT *dict_inline_open(const char *name, int open_flags, int dict_flags) */ if (DICT_NEED_UTF8_ACTIVATION(util_utf8_enable, dict_flags) && allascii(name) == 0 - && valid_utf8_string(name, strlen(name)) == 0) + && valid_utf8_stringz(name) == 0) DICT_INLINE_RETURN(dict_surrogate(DICT_TYPE_INLINE, name, open_flags, dict_flags, "bad UTF-8 syntax: \"%s:%s\"; " diff --git a/src/util/dict_thash.c b/src/util/dict_thash.c index 69eb17b..bae4a63 100644 --- a/src/util/dict_thash.c +++ b/src/util/dict_thash.c @@ -127,7 +127,7 @@ DICT *dict_thash_open(const char *path, int open_flags, int dict_flags) */ if ((dict->flags & DICT_FLAG_UTF8_ACTIVE) && allascii(STR(line_buffer)) == 0 - && valid_utf8_string(STR(line_buffer), LEN(line_buffer)) == 0) { + && valid_utf8_stringz(STR(line_buffer)) == 0) { msg_warn("%s, line %d: non-UTF-8 input \"%s\"" " -- ignoring this line", VSTREAM_PATH(fp), lineno, STR(line_buffer)); @@ -181,8 +181,8 @@ DICT *dict_thash_open(const char *path, int open_flags, int dict_flags) " is this an alias file?", path, lineno); /* - * Optionally treat the value as a filename, and replace the value - * with the BASE64-encoded content of the named file. + * Optionally treat the value as a filename, and replace the + * value with the BASE64-encoded content of the named file. */ if (dict_flags & DICT_FLAG_SRC_RHS_IS_FILE) { VSTRING *base64_buf; diff --git a/src/util/dict_utf8.c b/src/util/dict_utf8.c index f1fc65a..9bb6b7b 100644 --- a/src/util/dict_utf8.c +++ b/src/util/dict_utf8.c @@ -100,7 +100,7 @@ static char *dict_utf8_check_fold(DICT *dict, const char *string, /* * Validate UTF-8 without casefolding. */ - if (!allascii(string) && valid_utf8_string(string, strlen(string)) == 0) { + if (!allascii(string) && valid_utf8_stringz(string) == 0) { if (err) *err = "malformed UTF-8 or invalid codepoint"; return (0); @@ -123,7 +123,7 @@ static char *dict_utf8_check_fold(DICT *dict, const char *string, static int dict_utf8_check(const char *string, CONST_CHAR_STAR *err) { - if (!allascii(string) && valid_utf8_string(string, strlen(string)) == 0) { + if (!allascii(string) && valid_utf8_stringz(string) == 0) { if (err) *err = "malformed UTF-8 or invalid codepoint"; return (0); diff --git a/src/util/inet_prefix_top.c b/src/util/inet_prefix_top.c index 8d5af00..f35d5f0 100644 --- a/src/util/inet_prefix_top.c +++ b/src/util/inet_prefix_top.c @@ -164,6 +164,7 @@ int main(int argc, char **argv) msg_info("PASS %s/%d", str_name_code(af_map, tp->in_af), tp->in_prefix_len); } + myfree(act_prefix); } msg_info("PASS=%d FAIL=%d", pass, fail); return (fail > 0); diff --git a/src/util/logwriter.c b/src/util/logwriter.c index aea2767..4a18be3 100644 --- a/src/util/logwriter.c +++ b/src/util/logwriter.c @@ -21,6 +21,9 @@ /* const char *path, /* const char *buffer, /* ssize_t buflen) +/* +/* int set_logwriter_create_perms( +/* const char *mode) /* DESCRIPTION /* This module manages a logfile writer. /* @@ -38,6 +41,15 @@ /* logwriter_one_shot() combines all the above operations. The /* result is zero if successful, VSTREAM_EOF if any operation /* failed. +/* +/* set_logwriter_create_perms() sets the file permissions that +/* will be used when creating a logfile. Valid inputs are +/* "644", "640", and "600". Leading zeros are allowed and +/* ignored. +/* DIAGNOSTICS +/* Fatal error: logfile create error; warning: logfile permission +/* change error. set_logwriter_create_perms() returns the file +/* create permission if the request is valid, -1 otherwise. /* LICENSE /* .ad /* .fi @@ -66,10 +78,12 @@ #include <mymalloc.h> #include <safe_open.h> #include <vstream.h> +#include <name_code.h> /* * Application-specific. */ +static int logwriter_perms = 0600; /* logwriter_open_or_die - open logfile */ @@ -82,7 +96,7 @@ VSTREAM *logwriter_open_or_die(const char *path) #define NO_CHOWN (-1) #define NO_CHGRP (-1) - fp = safe_open(path, O_CREAT | O_WRONLY | O_APPEND, 0644, + fp = safe_open(path, O_CREAT | O_WRONLY | O_APPEND, logwriter_perms, NO_STATP, NO_CHOWN, NO_CHGRP, why); if (fp == 0) msg_fatal("open logfile '%s': %s", path, vstring_str(why)); @@ -122,3 +136,21 @@ int logwriter_one_shot(const char *path, const char *buf, ssize_t len) err |= logwriter_close(fp); return (err ? VSTREAM_EOF : 0); } + +/* set_logwriter_create_perms - logfile permission control */ + +int set_logwriter_create_perms(const char *mode_str) +{ + static const NAME_CODE sane_perms[] = { + "644", 0644, + "640", 0640, + "600", 0600, + 0, -1, + }; + int perms; + + if ((perms = name_code(sane_perms, NAME_CODE_FLAG_NONE, + mode_str + strspn(mode_str, "0"))) != -1) + logwriter_perms = perms; + return (perms); +} diff --git a/src/util/logwriter.h b/src/util/logwriter.h index f5266e4..c827d25 100644 --- a/src/util/logwriter.h +++ b/src/util/logwriter.h @@ -23,6 +23,7 @@ extern VSTREAM *logwriter_open_or_die(const char *); extern int logwriter_write(VSTREAM *, const char *, ssize_t); extern int logwriter_close(VSTREAM *); extern int logwriter_one_shot(const char *, const char *, ssize_t); +extern int set_logwriter_create_perms(const char *); /* LICENSE /* .ad diff --git a/src/util/midna_domain.c b/src/util/midna_domain.c index 333a5c9..bc016b6 100644 --- a/src/util/midna_domain.c +++ b/src/util/midna_domain.c @@ -178,7 +178,7 @@ static void *midna_domain_to_ascii_create(const char *name, void *unused_context /* * Paranoia: do not expose uidna_*() to unfiltered network data. */ - if (allascii(name) == 0 && valid_utf8_string(name, strlen(name)) == 0) { + if (allascii(name) == 0 && valid_utf8_stringz(name) == 0) { msg_warn("%s: Problem translating domain \"%.100s\" to ASCII form: %s", myname, name, "malformed UTF-8"); return (0); @@ -232,7 +232,7 @@ static void *midna_domain_to_utf8_create(const char *name, void *unused_context) /* * Paranoia: do not expose uidna_*() to unfiltered network data. */ - if (allascii(name) == 0 && valid_utf8_string(name, strlen(name)) == 0) { + if (allascii(name) == 0 && valid_utf8_stringz(name) == 0) { msg_warn("%s: Problem translating domain \"%.100s\" to UTF-8 form: %s", myname, name, "malformed UTF-8"); return (0); diff --git a/src/util/parse_utf8_char.h b/src/util/parse_utf8_char.h new file mode 100644 index 0000000..b00a1c2 --- /dev/null +++ b/src/util/parse_utf8_char.h @@ -0,0 +1,122 @@ +/*++ +/* NAME +/* parse_utf8_char 3h +/* SUMMARY +/* parse one UTF-8 multibyte character +/* SYNOPSIS +/* #include <parse_utf8_char.h> +/* +/* char *parse_utf8_char(str, end) +/* const char *str; +/* const char *end; +/* DESCRIPTION +/* parse_utf8_char() determines if the byte sequence starting +/* at \fBstr\fR begins with a complete UTF-8 character as +/* defined in RFC 3629. That is, a proper encoding of code +/* points U+0000..U+10FFFF, excluding over-long encodings and +/* excluding U+D800..U+DFFF surrogates. +/* +/* When the byte sequence starting at \fBstr\fR begins with a +/* complete UTF-8 character, this function returns a pointer +/* to the last byte in that character. Otherwise, it returns +/* a null pointer. +/* +/* The \fBend\fR argument is either null (the byte sequence +/* starting at \fBstr\fR must be null terminated), or \fBend +/* - str\fR specifies the length of the byte sequence. +/* BUGS +/* Code points in the range U+FDD0..U+FDEF and ending in FFFE +/* or FFFF are non-characters in UNICODE. This function does +/* not reject these. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* porcupine.org +/* Amawalk, NY 10501, USA +/*--*/ + + /* + * System library. + */ +#include <sys_defs.h> + +#ifdef NO_INLINE +#define inline /* */ +#endif + +/* parse_utf8_char - parse and validate one UTF8 multibyte sequence */ + +static inline char *parse_utf8_char(const char *str, const char *end) +{ + const unsigned char *cp = (const unsigned char *) str; + const unsigned char *ep = (const unsigned char *) end; + unsigned char c0, ch; + + /* + * Optimized for correct input, time, space, and for CPUs that have a + * decent number of registers. Other implementation considerations: + * + * - In the UTF-8 encoding, a non-leading byte is never null. Therefore, + * this function will correctly reject a partial UTF-8 character at the + * end of a null-terminated string. + * + * - If the "end" argument is a null constant, and if this function is + * inlined, then an optimizing compiler should propagate the constant + * through the "ep" variable, and eliminate any code branches that + * require ep != 0. + */ + /* Single-byte encodings. */ + if (EXPECTED((c0 = *cp) <= 0x7f) /* we know that c0 >= 0x0 */ ) { + return ((char *) cp); + } + /* Two-byte encodings. */ + else if (EXPECTED(c0 <= 0xdf) /* we know that c0 >= 0x80 */ ) { + /* Exclude over-long encodings. */ + if (UNEXPECTED(c0 < 0xc2) + || UNEXPECTED(ep && cp + 1 >= ep) + /* Require UTF-8 tail byte. */ + || UNEXPECTED(((ch = *++cp) & 0xc0) != 0x80)) + return (0); + return ((char *) cp); + } + /* Three-byte encodings. */ + else if (EXPECTED(c0 <= 0xef) /* we know that c0 >= 0xe0 */ ) { + if (UNEXPECTED(ep && cp + 2 >= ep) + /* Exclude over-long encodings. */ + || UNEXPECTED((ch = *++cp) < (c0 == 0xe0 ? 0xa0 : 0x80)) + /* Exclude U+D800..U+DFFF. */ + || UNEXPECTED(ch > (c0 == 0xed ? 0x9f : 0xbf)) + /* Require UTF-8 tail byte. */ + || UNEXPECTED(((ch = *++cp) & 0xc0) != 0x80)) + return (0); + return ((char *) cp); + } + /* Four-byte encodings. */ + else if (EXPECTED(c0 <= 0xf4) /* we know that c0 >= 0xf0 */ ) { + if (UNEXPECTED(ep && cp + 3 >= ep) + /* Exclude over-long encodings. */ + || UNEXPECTED((ch = *++cp) < (c0 == 0xf0 ? 0x90 : 0x80)) + /* Exclude code points above U+10FFFF. */ + || UNEXPECTED(ch > (c0 == 0xf4 ? 0x8f : 0xbf)) + /* Require UTF-8 tail byte. */ + || UNEXPECTED(((ch = *++cp) & 0xc0) != 0x80) + /* Require UTF-8 tail byte. */ + || UNEXPECTED(((ch = *++cp) & 0xc0) != 0x80)) + return (0); + return ((char *) cp); + } + /* Invalid: c0 >= 0xf5 */ + else { + return (0); + } +} + +#undef inline diff --git a/src/util/printable.c b/src/util/printable.c index 6c148fd..0e1ae19 100644 --- a/src/util/printable.c +++ b/src/util/printable.c @@ -45,6 +45,10 @@ /* Google, Inc. /* 111 8th Avenue /* New York, NY 10011, USA +/* +/* Wietse Venema +/* porcupine.org +/* Amawalk, NY 10501, USA /*--*/ /* System library. */ @@ -56,8 +60,9 @@ /* Utility library. */ #include "stringops.h" +#include "parse_utf8_char.h" -int util_utf8_enable = 0; +int util_utf8_enable = 0; /* printable - binary compatibility */ @@ -74,27 +79,150 @@ char *printable(char *string, int replacement) char *printable_except(char *string, int replacement, const char *except) { - unsigned char *cp; + char *cp; + char *last; int ch; /* - * XXX Replace invalid UTF8 sequences (too short, over-long encodings, - * out-of-range code points, etc). See valid_utf8_string.c. + * In case of a non-UTF8 sequence (bad leader byte, bad non-leader byte, + * over-long encodings, out-of-range code points, etc), replace the first + * byte, and try to resynchronize at the next byte. */ - cp = (unsigned char *) string; - while ((ch = *cp) != 0) { - if (ISASCII(ch) && (ISPRINT(ch) || (except && strchr(except, ch)))) { - /* ok */ - } else if (util_utf8_enable && ch >= 194 && ch <= 254 - && cp[1] >= 128 && cp[1] < 192) { - /* UTF8; skip the rest of the bytes in the character. */ - while (cp[1] >= 128 && cp[1] < 192) - cp++; - } else { - /* Not ASCII and not UTF8. */ - *cp = replacement; +#define PRINT_OR_EXCEPT(ch) (ISPRINT(ch) || (except && strchr(except, ch))) + + for (cp = string; (ch = *(unsigned char *) cp) != 0; cp++) { + if (util_utf8_enable == 0) { + if (ISASCII(ch) && PRINT_OR_EXCEPT(ch)) + continue; + } else if ((last = parse_utf8_char(cp, 0)) == cp) { /* ASCII */ + if (PRINT_OR_EXCEPT(ch)) + continue; + } else if (last != 0) { /* Other UTF8 */ + cp = last; + continue; } - cp++; + *cp = replacement; } return (string); } + +#ifdef TEST + +#include <stdlib.h> +#include <string.h> +#include <msg.h> +#include <msg_vstream.h> +#include <mymalloc.h> +#include <vstream.h> + + /* + * Test cases for 1-, 2-, and 3-byte encodings. Originally contributed by + * Viktor Dukhovni, and annotated using translate.google.com. + * + * See valid_utf8_string.c for single-error tests. + * + * XXX Need a test for 4-byte encodings, preferably with strings that can be + * displayed. + */ +struct testcase { + const char *name; + const char *input; + const char *expected;; +}; +static const struct testcase testcases[] = { + {"Printable ASCII", + "printable", "printable" + }, + {"ASCII with control character", + "non\bn-printable", "non?n-printable" + }, + {"Latin accented text, no error", + "na\303\257ve", "na\303\257ve" + }, + {"Latin text, with error", + "na\303ve", "na?ve" + }, + {"Viktor, Cyrillic, no error", + "\320\262\320\270\320\272\321\202\320\276\321\200", + "\320\262\320\270\320\272\321\202\320\276\321\200" + }, + {"Viktor, Cyrillic, two errors", + "\320\262\320\320\272\272\321\202\320\276\321\200", + "\320\262?\320\272?\321\202\320\276\321\200" + }, + {"Viktor, Hebrew, no error", + "\327\225\327\231\327\247\327\230\327\225\326\274\327\250", + "\327\225\327\231\327\247\327\230\327\225\326\274\327\250" + }, + {"Viktor, Hebrew, with error", + "\327\225\231\327\247\327\230\327\225\326\274\327\250", + "\327\225?\327\247\327\230\327\225\326\274\327\250" + }, + {"Chinese (Simplified), no error", + "\344\270\255\345\233\275\344\272\222\350\201\224\347\275\221\347" + "\273\234\345\217\221\345\261\225\347\212\266\345\206\265\347\273" + "\237\350\256\241\346\212\245\345\221\212", + "\344\270\255\345\233\275\344\272\222\350\201\224\347\275\221\347" + "\273\234\345\217\221\345\261\225\347\212\266\345\206\265\347\273" + "\237\350\256\241\346\212\245\345\221\212" + }, + {"Chinese (Simplified), with errors", + "\344\270\255\345\344\272\222\350\224\347\275\221\347" + "\273\234\345\217\221\345\261\225\347\212\266\345\206\265\347\273" + "\237\350\256\241\346\212\245\345", + "\344\270\255?\344\272\222??\347\275\221\347" + "\273\234\345\217\221\345\261\225\347\212\266\345\206\265\347\273" + "\237\350\256\241\346\212\245?" + }, +}; + +int main(int argc, char **argv) +{ + const struct testcase *tp; + 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++) { + char *input; + char *actual; + int ok = 0; + + /* + * Notes: + * + * - The input is modified, therefore it must be copied. + * + * - The msg(3) functions use printable() which interferes when logging + * inputs and outputs. Use vstream_fprintf() instead. + */ + vstream_fprintf(VSTREAM_ERR, "RUN %s\n", tp->name); + input = mystrdup(tp->input); + actual = printable(input, '?'); + + if (strcmp(actual, tp->expected) != 0) { + vstream_fprintf(VSTREAM_ERR, "input: >%s<, got: >%s<, want: >%s<\n", + tp->input, actual, tp->expected); + } 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); + } + msg_info("PASS=%d FAIL=%d", pass, fail); + return (fail > 0); +} + +#endif diff --git a/src/util/quote_for_json.c b/src/util/quote_for_json.c new file mode 100644 index 0000000..f54af3f --- /dev/null +++ b/src/util/quote_for_json.c @@ -0,0 +1,218 @@ +/*++ +/* NAME +/* quote_for_json 3 +/* SUMMARY +/* quote UTF-8 string value for JSON +/* SYNOPSIS +/* #include <quote_for_json.h> +/* +/* char *quote_for_json( +/* VSTRING *result, +/* const char *in, +/* ssize_t len) +/* +/* char *quote_for_json_append( +/* VSTRING *result, +/* const char *in, +/* ssize_t len) +/* DESCRIPTION +/* quote_for_json() takes well-formed UTF-8 encoded text, +/* quotes that text compliant with RFC 4627, and returns a +/* pointer to the resulting text. The input may contain null +/* bytes, but the output will not. +/* +/* quote_for_json() produces short (two-letter) escape sequences +/* for common control characters, double quote and backslash. +/* It will not quote "/" (0x2F), and will quote DEL (0x7f) as +/* \u007F to make it printable. The input byte sequence "\uXXXX" +/* is quoted like any other text (the "\" is escaped as "\\"). +/* +/* quote_for_json() does not perform UTF-8 validation. The caller +/* should use valid_utf8_string() or printable() as appropriate. +/* +/* quote_for_json_append() appends the output to the result buffer. +/* +/* Arguments: +/* .IP result +/* Storage for the result, resized automatically. +/* .IP in +/* Pointer to the input byte sequence. +/* .IP len +/* The length of the input byte sequence, or a negative number +/* when the byte sequence is null-terminated. +/* DIAGNOSTICS +/* Fatal error: memory allocation error. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/* +/* Wietse Venema +/* porcupine.org +/*--*/ + + /* + * System library. + */ +#include <sys_defs.h> +#include <ctype.h> +#include <string.h> + + /* + * Utility library. + */ +#include <stringops.h> +#include <vstring.h> + +#define STR(x) vstring_str(x) + +/* quote_for_json_append - quote JSON string, append result */ + +char *quote_for_json_append(VSTRING *result, const char *text, ssize_t len) +{ + const char *cp; + int ch; + + if (len < 0) + len = strlen(text); + + for (cp = text; len > 0; len--, cp++) { + ch = *(const unsigned char *) cp; + if (UNEXPECTED(ISCNTRL(ch))) { + switch (ch) { + case '\b': + VSTRING_ADDCH(result, '\\'); + VSTRING_ADDCH(result, 'b'); + break; + case '\f': + VSTRING_ADDCH(result, '\\'); + VSTRING_ADDCH(result, 'f'); + break; + case '\n': + VSTRING_ADDCH(result, '\\'); + VSTRING_ADDCH(result, 'n'); + break; + case '\r': + VSTRING_ADDCH(result, '\\'); + VSTRING_ADDCH(result, 'r'); + break; + case '\t': + VSTRING_ADDCH(result, '\\'); + VSTRING_ADDCH(result, 't'); + break; + default: + /* All other controls including DEL and NUL. */ + vstring_sprintf_append(result, "\\u%04X", ch); + break; + } + } else { + switch (ch) { + case '\\': + case '"': + VSTRING_ADDCH(result, '\\'); + /* FALLTHROUGH */ + default: + /* Includes malformed UTF-8. */ + VSTRING_ADDCH(result, ch); + break; + } + } + } + VSTRING_TERMINATE(result); + return (STR(result)); +} + +/* quote_for_json - quote JSON string */ + +char *quote_for_json(VSTRING *result, const char *text, ssize_t len) +{ + VSTRING_RESET(result); + return (quote_for_json_append(result, text, len)); +} + +#ifdef TEST + + /* + * System library. + */ +#include <stdlib.h> + + /* + * Utility library. + */ +#include <msg.h> +#include <msg_vstream.h> + +typedef struct TEST_CASE { + const char *label; /* identifies test case */ + char *(*fn) (VSTRING *, const char *, ssize_t); + const char *input; /* input string */ + ssize_t input_len; /* -1 or input length */ + const char *exp_res; /* expected result */ +} TEST_CASE; + +#define PASS (0) +#define FAIL (1) + + /* + * The test cases. + */ +static const TEST_CASE test_cases[] = { + {"ordinary ASCII text", quote_for_json, + " abcABC012.,[]{}/", -1, " abcABC012.,[]{}/", + }, + {"quote_for_json_append", quote_for_json_append, + "foo", -1, " abcABC012.,[]{}/foo", + }, + {"common control characters", quote_for_json, + "\b\f\r\n\t", -1, "\\b\\f\\r\\n\\t", + }, + {"uncommon control characters and DEL", quote_for_json, + "\0\01\037\040\176\177", 6, "\\u0000\\u0001\\u001F ~\\u007F", + }, + {"malformed UTF-8", quote_for_json, + "\\*\\uasd\\u007F\x80", -1, "\\\\*\\\\uasd\\\\u007F\x80", + }, + 0, +}; + +int main(int argc, char **argv) +{ + const TEST_CASE *tp; + int pass = 0; + int fail = 0; + VSTRING *res_buf = vstring_alloc(100); + + msg_vstream_init(sane_basename((VSTRING *) 0, argv[0]), VSTREAM_ERR); + + for (tp = test_cases; tp->label != 0; tp++) { + int test_fail = 0; + char *res; + + msg_info("RUN %s", tp->label); + res = tp->fn(res_buf, tp->input, tp->input_len); + if (strcmp(res, tp->exp_res) != 0) { + msg_warn("test case '%s': got '%s', want '%s'", + tp->label, res, tp->exp_res); + test_fail = 1; + } + if (test_fail) { + fail++; + msg_info("FAIL %s", tp->label); + test_fail = 1; + } else { + msg_info("PASS %s", tp->label); + pass++; + } + } + msg_info("PASS=%d FAIL=%d", pass, fail); + vstring_free(res_buf); + exit(fail != 0); +} + +#endif diff --git a/src/util/readlline.c b/src/util/readlline.c index 015877a..721b75f 100644 --- a/src/util/readlline.c +++ b/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) @@ -119,6 +128,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 * parsing routine. Do not abort, just warn, so that critical programs @@ -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 <stdlib.h> +#include <string.h> +#include <msg.h> +#include <msg_vstream.h> +#include <stringops.h> +#include <vstream.h> +#include <vstring.h> + + /* + * 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/src/util/stringops.h b/src/util/stringops.h index 97aa597..db56f23 100644 --- a/src/util/stringops.h +++ b/src/util/stringops.h @@ -60,10 +60,13 @@ extern int allascii_len(const char *, ssize_t); extern const char *WARN_UNUSED_RESULT split_nameval(char *, char **, char **); extern const char *WARN_UNUSED_RESULT split_qnameval(char *, char **, char **); extern int valid_utf8_string(const char *, ssize_t); +extern int valid_utf8_stringz(const char *); extern size_t balpar(const char *, const char *); extern char *WARN_UNUSED_RESULT extpar(char **, const char *, int); extern int strcasecmp_utf8x(int, const char *, const char *); extern int strncasecmp_utf8x(int, const char *, const char *, ssize_t); +extern char *quote_for_json(VSTRING *, const char *, ssize_t); +extern char *quote_for_json_append(VSTRING *, const char *, ssize_t); #define EXTPAR_FLAG_NONE (0) #define EXTPAR_FLAG_STRIP (1<<0) /* "{ text }" -> "text" */ diff --git a/src/util/sys_defs.h b/src/util/sys_defs.h index 9247185..62749ab 100644 --- a/src/util/sys_defs.h +++ b/src/util/sys_defs.h @@ -1332,6 +1332,13 @@ extern int dup2_pass_on_exec(int oldd, int newd); #endif /* + * The RFC 5322 Date and Time Specification recommends single space between + * date-time tokens. To avoid breaking change, format all numerical days as + * two-digit days (i.e. days 1-9 now have a leading zero instead of space). + */ +#define TWO_DIGIT_DAY_IN_DATE_TIME + + /* * Check for required but missing definitions. */ #if !defined(HAS_FCNTL_LOCK) && !defined(HAS_FLOCK_LOCK) diff --git a/src/util/valid_hostname.c b/src/util/valid_hostname.c index 8b234c4..457d1f1 100644 --- a/src/util/valid_hostname.c +++ b/src/util/valid_hostname.c @@ -6,9 +6,9 @@ /* SYNOPSIS /* #include <valid_hostname.h> /* -/* int valid_hostname(name, gripe) +/* int valid_hostname(name, flags) /* const char *name; -/* int gripe; +/* int flags; /* /* int valid_hostaddr(addr, gripe) /* const char *addr; @@ -32,6 +32,10 @@ /* dots, no leading or trailing dots or hyphens, no labels /* longer than VALID_LABEL_LEN characters, and it should not /* be all numeric. +/* The flags argument is the bit-wise or of zero or more of +/* DO_GRIPE or DO_WILDCARD (the latter allows the "*." name +/* prefix, which is rare but valid in some DNS responses and +/* queries). /* /* valid_hostaddr() requires that the input is a valid string /* representation of an IPv4 or IPv6 network address as @@ -403,8 +407,9 @@ int main(int unused_argc, char **argv) while (vstring_fgets_nonl(buffer, VSTREAM_IN)) { msg_info("testing: \"%s\"", vstring_str(buffer)); - valid_hostname(vstring_str(buffer), DO_GRIPE); - valid_hostaddr(vstring_str(buffer), DO_GRIPE); + valid_hostname(vstring_str(buffer), DO_GRIPE | DO_WILDCARD); + if (strchr(vstring_str(buffer), '*') == 0) + valid_hostaddr(vstring_str(buffer), DO_GRIPE); } exit(0); } diff --git a/src/util/valid_hostname.in b/src/util/valid_hostname.in index 608c0d1..4cdf019 100644 --- a/src/util/valid_hostname.in +++ b/src/util/valid_hostname.in @@ -53,3 +53,9 @@ g:a:a:a:a:a:a:a a::b :a::b a::b: +*.foo.bar +*foo.bar +foo.*.bar +foo*bar +foo.bar* +* diff --git a/src/util/valid_hostname.ref b/src/util/valid_hostname.ref index 08b23b8..eccc558 100644 --- a/src/util/valid_hostname.ref +++ b/src/util/valid_hostname.ref @@ -141,3 +141,13 @@ ./valid_hostname: testing: "a::b:" ./valid_hostname: warning: valid_hostname: invalid character 58(decimal): a::b: ./valid_hostname: warning: valid_ipv6_hostaddr: bad null last field in IPv6 address: a::b: +./valid_hostname: testing: "*.foo.bar" +./valid_hostname: testing: "*foo.bar" +./valid_hostname: warning: valid_hostname: '*' can be the first label only: *foo.bar +./valid_hostname: testing: "foo.*.bar" +./valid_hostname: warning: valid_hostname: '*' can be the first label only: foo.*.bar +./valid_hostname: testing: "foo*bar" +./valid_hostname: warning: valid_hostname: '*' can be the first label only: foo*bar +./valid_hostname: testing: "foo.bar*" +./valid_hostname: warning: valid_hostname: '*' can be the first label only: foo.bar* +./valid_hostname: testing: "*" diff --git a/src/util/valid_utf8_string.c b/src/util/valid_utf8_string.c index 96b5b4d..f5b4ff4 100644 --- a/src/util/valid_utf8_string.c +++ b/src/util/valid_utf8_string.c @@ -9,24 +9,24 @@ /* int valid_utf8_string(str, len) /* const char *str; /* ssize_t len; +/* +/* int valid_utf8_stringz(str) +/* const char *str; +/* ssize_t len; /* DESCRIPTION -/* valid_utf8_string() determines if a string satisfies the UTF-8 -/* definition in RFC 3629. That is, it contains proper encodings -/* of code points U+0000..U+10FFFF, excluding over-long encodings -/* and excluding U+D800..U+DFFF surrogates. +/* valid_utf8_string() determines if all bytes in a string +/* satisfy parse_utf8_char(3h) checks. See there for any +/* implementation limitations. +/* +/* valid_utf8_stringz() determines the same for zero-terminated +/* strings. /* /* A zero-length string is considered valid. /* DIAGNOSTICS /* The result value is zero when the caller specifies a negative -/* length, or a string that violates RFC 3629, for example a -/* string that is truncated in the middle of a multi-byte -/* sequence. -/* BUGS -/* But wait, there is more. Code points in the range U+FDD0..U+FDEF -/* and ending in FFFE or FFFF are non-characters in UNICODE. This -/* function does not block these. +/* length, or a string that does not pass parse_utf8_char(3h) checks. /* SEE ALSO -/* RFC 3629 +/* parse_utf8_char(3h), parse one UTF-8 multibyte character /* LICENSE /* .ad /* .fi @@ -36,6 +36,10 @@ /* IBM T.J. Watson Research /* P.O. Box 704 /* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* porcupine.org +/* Amawalk, NY 10501, USA /*--*/ /* System library. */ @@ -45,66 +49,50 @@ /* Utility library. */ #include <stringops.h> +#include <parse_utf8_char.h> /* valid_utf8_string - validate string according to RFC 3629 */ int valid_utf8_string(const char *str, ssize_t len) { - const unsigned char *end = (const unsigned char *) str + len; - const unsigned char *cp; - unsigned char c0, ch; + const char *ep = str + len; + const char *cp; + const char *last; if (len < 0) return (0); - if (len <= 0) + if (len == 0) return (1); /* - * Optimized for correct input, time, space, and for CPUs that have a - * decent number of registers. + * Ideally, the compiler will inline parse_utf8_char(). */ - for (cp = (const unsigned char *) str; cp < end; cp++) { - /* Single-byte encodings. */ - if (EXPECTED((c0 = *cp) <= 0x7f) /* we know that c0 >= 0x0 */ ) { - /* void */ ; - } - /* Two-byte encodings. */ - else if (EXPECTED(c0 <= 0xdf) /* we know that c0 >= 0x80 */ ) { - /* Exclude over-long encodings. */ - if (UNEXPECTED(c0 < 0xc2) - || UNEXPECTED(cp + 1 >= end) - /* Require UTF-8 tail byte. */ - || UNEXPECTED(((ch = *++cp) & 0xc0) != 0x80)) - return (0); - } - /* Three-byte encodings. */ - else if (EXPECTED(c0 <= 0xef) /* we know that c0 >= 0xe0 */ ) { - if (UNEXPECTED(cp + 2 >= end) - /* Exclude over-long encodings. */ - || UNEXPECTED((ch = *++cp) < (c0 == 0xe0 ? 0xa0 : 0x80)) - /* Exclude U+D800..U+DFFF. */ - || UNEXPECTED(ch > (c0 == 0xed ? 0x9f : 0xbf)) - /* Require UTF-8 tail byte. */ - || UNEXPECTED(((ch = *++cp) & 0xc0) != 0x80)) - return (0); - } - /* Four-byte encodings. */ - else if (EXPECTED(c0 <= 0xf4) /* we know that c0 >= 0xf0 */ ) { - if (UNEXPECTED(cp + 3 >= end) - /* Exclude over-long encodings. */ - || UNEXPECTED((ch = *++cp) < (c0 == 0xf0 ? 0x90 : 0x80)) - /* Exclude code points above U+10FFFF. */ - || UNEXPECTED(ch > (c0 == 0xf4 ? 0x8f : 0xbf)) - /* Require UTF-8 tail byte. */ - || UNEXPECTED(((ch = *++cp) & 0xc0) != 0x80) - /* Require UTF-8 tail byte. */ - || UNEXPECTED(((ch = *++cp) & 0xc0) != 0x80)) - return (0); - } - /* Invalid: c0 >= 0xf5 */ - else { + for (cp = str; cp < ep; cp++) { + if ((last = parse_utf8_char(cp, ep)) != 0) + cp = last; + else + return (0); + } + return (1); +} + +/* valid_utf8_stringz - validate string according to RFC 3629 */ + +int valid_utf8_stringz(const char *str) +{ + const char *cp; + const char *last; + + /* + * Ideally, the compiler will inline parse_utf8_char(), propagate the + * null pointer constant value, and eliminate code branches that test + * whether 0 != 0. + */ + for (cp = str; *cp; cp++) { + if ((last = parse_utf8_char(cp, 0)) != 0) + cp = last; + else return (0); - } } return (1); } @@ -114,26 +102,139 @@ int valid_utf8_string(const char *str, ssize_t len) */ #ifdef TEST #include <stdlib.h> +#include <string.h> +#include <msg.h> #include <vstream.h> -#include <vstring.h> -#include <vstring_vstream.h> +#include <msg_vstream.h> + + /* + * Test cases for 1-, 2-, and 3-byte encodings. See printable.c for UTF8 + * parser resychronization tests. + * + * XXX Need a test for 4-byte encodings, preferably with strings that can be + * displayed. + * + * XXX Need tests with hand-crafted over-long encodings and surrogates. + */ +struct testcase { + const char *name; + const char *input; + int expected; +}; -#define STR(x) vstring_str(x) -#define LEN(x) VSTRING_LEN(x) +#define T_VALID (1) +#define T_INVALID (0) +#define valid_to_str(v) ((v) ? "VALID" : "INVALID") -int main(void) +static const struct testcase testcases[] = { + {"Printable ASCII", + "printable", T_VALID, + }, + {"Latin script, accented, no error", + "na\303\257ve", T_VALID, + }, + {"Latin script, accented, missing non-leading byte", + "na\303ve", T_INVALID, + }, + {"Latin script, accented, missing leading byte", + "na\257ve", T_INVALID, + }, + {"Viktor, Cyrillic, no error", + "\320\262\320\270\320\272\321\202\320\276\321\200", T_VALID, + }, + {"Viktor, Cyrillic, missing non-leading byte", + "\320\262\320\320\272\321\202\320\276\321\200", T_INVALID, + }, + {"Viktor, Cyrillic, missing leading byte", + "\320\262\270\320\272\321\202\320\276\321\200", T_INVALID, + }, + {"Viktor, Cyrillic, truncated", + "\320\262\320\270\320\272\321\202\320\276\321", T_INVALID, + }, + {"Viktor, Hebrew, no error", + "\327\225\327\231\327\247\327\230\327\225\326\274\327\250", T_VALID, + }, + {"Viktor, Hebrew, missing leading byte", + "\327\225\231\327\247\327\230\327\225\326\274\327\250", T_INVALID, + }, + {"Chinese (Simplified), no error", + "\344\270\255\345\233\275\344\272\222\350\201\224\347\275\221\347" + "\273\234\345\217\221\345\261\225\347\212\266\345\206\265\347\273" + "\237\350\256\241\346\212\245\345\221\212", T_VALID, + }, + {"Chinese (Simplified), missing leading byte", + "\344\270\255\345\233\275\344\272\222\350\201\224\275\221\347" + "\273\234\345\217\221\345\261\225\347\212\266\345\206\265\347\273" + "\237\350\256\241\346\212\245\345\221\212", T_INVALID, + }, + {"Chinese (Simplified), missing first non-leading byte", + "\344\270\255\345\233\275\344\272\222\350\201\224\347\221\347" + "\273\234\345\217\221\345\261\225\347\212\266\345\206\265\347\273" + "\237\350\256\241\346\212\245\345\221\212", T_INVALID, + }, + {"Chinese (Simplified), missing second non-leading byte", + "\344\270\255\345\233\275\344\272\222\350\201\224\347\275\347" + "\273\234\345\217\221\345\261\225\347\212\266\345\206\265\347\273" + "\237\350\256\241\346\212\245\345\221\212", T_INVALID, + }, + {"Chinese (Simplified), truncated", + "\344\270\255\345\233\275\344\272\222\350\201\224\347\275\221\347" + "\273\234\345\217\221\345\261\225\347\212\266\345\206\265\347\273" + "\237\350\256\241\346\212\245\345", T_INVALID, + }, +}; + +int main(int argc, char **argv) { - VSTRING *buf = vstring_alloc(1); + const struct testcase *tp; + 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 actual_l; + int actual_z; + int ok = 0; - while (vstring_get_nonl(buf, VSTREAM_IN) != VSTREAM_EOF) { - vstream_printf("%c", (LEN(buf) && !valid_utf8_string(STR(buf), LEN(buf))) ? - '!' : ' '); - vstream_fwrite(VSTREAM_OUT, STR(buf), LEN(buf)); - vstream_printf("\n"); + /* + * Notes: + * + * - The msg(3) functions use printable() which interferes when logging + * inputs and outputs. Use vstream_fprintf() instead. + */ + vstream_fprintf(VSTREAM_ERR, "RUN %s\n", tp->name); + actual_l = valid_utf8_string(tp->input, strlen(tp->input)); + actual_z = valid_utf8_stringz(tp->input); + + if (actual_l != tp->expected) { + vstream_fprintf(VSTREAM_ERR, + "input: >%s<, 'actual_l' got: >%s<, want: >%s<\n", + tp->input, valid_to_str(actual_l), + valid_to_str(tp->expected)); + } 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)); + } 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++; + } } - vstream_fflush(VSTREAM_OUT); - vstring_free(buf); - exit(0); + msg_info("PASS=%d FAIL=%d", pass, fail); + return (fail > 0); } #endif diff --git a/src/util/vstream.c b/src/util/vstream.c index b4f9fbb..affbcc0 100644 --- a/src/util/vstream.c +++ b/src/util/vstream.c @@ -522,6 +522,7 @@ /* System library. */ #include <sys_defs.h> +#include <sys/stat.h> #include <stdlib.h> /* 44BSD stdarg.h uses abort() */ #include <stdarg.h> #include <stddef.h> @@ -1386,7 +1387,38 @@ VSTREAM *vstream_fopen(const char *path, int flags, mode_t mode) VSTREAM *stream; int fd; - if ((fd = open(path, flags, mode)) < 0) { + /* + * To set permissions on new files only, we need to distinguish between + * creating a new file and opening an existing one. + */ +#define open_create(path, flags, mode) \ + open((path), (flags) | (O_CREAT | O_EXCL), (mode)) +#define open_exist(path, flags, mode) \ + open((path), (flags) & ~(O_CREAT | O_EXCL), (mode)) + + switch (flags & (O_CREAT | O_EXCL)) { + case O_CREAT: + fd = open_exist(path, flags, mode); + if (fd < 0 && errno == ENOENT) { + fd = open_create(path, flags, mode); + if (fd >= 0) { + if (fchmod(fd, mode) < 0) /* can't uncreate */ + msg_warn("fchmod %s 0%o: %m", path, (unsigned) mode); + } else if ( /* fd < 0 && */ errno == EEXIST) + fd = open_exist(path, flags, mode); + } + break; + case O_CREAT | O_EXCL: + fd = open(path, flags, mode); + if (fd >= 0) + if (fchmod(fd, mode) < 0) /* can't uncreate */ + msg_warn("fchmod %s 0%o: %m", path, (unsigned) mode); + break; + default: + fd = open(path, flags, mode); + break; + } + if (fd < 0) { return (0); } else { stream = vstream_fdopen(fd, flags); diff --git a/src/xsasl/xsasl_cyrus_server.c b/src/xsasl/xsasl_cyrus_server.c index 4bf2ed2..e903289 100644 --- a/src/xsasl/xsasl_cyrus_server.c +++ b/src/xsasl/xsasl_cyrus_server.c @@ -52,6 +52,10 @@ /* Google, Inc. /* 111 8th Avenue /* New York, NY 10011, USA +/* +/* Wietse Venema +/* porcupine.org +/* Amawalk, NY 10501, USA /*--*/ /* System library. */ diff --git a/src/xsasl/xsasl_server.c b/src/xsasl/xsasl_server.c index e8d7e16..c504864 100644 --- a/src/xsasl/xsasl_server.c +++ b/src/xsasl/xsasl_server.c @@ -123,7 +123,10 @@ /* reply. /* /* xsasl_server_get_username() returns the stored username -/* after successful authentication. +/* after successful authentication. The username may be null +/* after authentication failure, depending on the kind of +/* failure and on authentication backend implementation +/* details. A non-null result is converted to printable text. /* /* Arguments: /* .IP addr_family @@ -207,6 +210,10 @@ /* Google, Inc. /* 111 8th Avenue /* New York, NY 10011, USA +/* +/* Wietse Venema +/* porcupine.org +/* Amawalk, NY 10501, USA /*--*/ /* System library. */ |