From: Wietse Venema Date: Tue, 16 Aug 2022 05:00:00 +0000 (-0500) Subject: postfix-3.8-20220816-nonprod X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f4b71534c0527621af99182eb43fdf29303c9bed;p=thirdparty%2Fpostfix.git postfix-3.8-20220816-nonprod --- diff --git a/postfix/.indent.pro b/postfix/.indent.pro index 7a382f767..0c4dfbefd 100644 --- a/postfix/.indent.pro +++ b/postfix/.indent.pro @@ -1,5 +1,4 @@ -TABOUNCE_STATE --Taddrinfo -TADDR_MATCH_LIST -TADDR_PATTERN -TALIAS_TOKEN @@ -21,7 +20,6 @@ -TBH_TABLE -TBINATTR -TBINATTR_INFO --Tbind_props -TBINHASH -TBINHASH_INFO -TBIO @@ -39,10 +37,9 @@ -TBYTE_MASK -TCFG_PARSER -TCIDR_MATCH --Tcipher_probe_t -TCLEANUP_REGION --TCLEANUP_STAT_DETAIL -TCLEANUP_STATE +-TCLEANUP_STAT_DETAIL -TCLIENT_LIST -TCLNT_STREAM -TCONFIG_BOOL_FN_TABLE @@ -66,12 +63,9 @@ -TCRYPTO_EX_DATA -TCTABLE -TCTABLE_ENTRY --Td2i_X509_t --Tdane_digest --Tdane_mtype -TDB_COMMON_CTX --TDELIVER_ATTR -TDELIVERED_HDR_INFO +-TDELIVER_ATTR -TDELIVER_REQUEST -TDELTA_TIME -TDICT @@ -157,9 +151,7 @@ -TEVP_PKEY -TEXPAND_ATTR -TFILE --Tfilter_ctx -TFORWARD_INFO --Tgeneral_name_stack_t -THBC_ACTION_CALL_BACKS -THBC_CALL_BACKS -THBC_CHECKS @@ -172,19 +164,18 @@ -THOST -THTABLE -THTABLE_INFO --Tiana_digest -TINET_ADDR_LIST -TINET_PROTO_INFO -TINSTANCE -TINST_SELECTION -TINT32_TYPE --TINT_TABLE -TINTV +-TINT_TABLE -TJMP_BUF_WRAPPER -TLDAP --TLDAP_CONN -TLDAPMessage -TLDAPURLDesc +-TLDAP_CONN -TLIB_DP -TLIB_FN -TLMTP_ATTR @@ -200,14 +191,14 @@ -TMAC_EXP_OP_INFO -TMAC_HEAD -TMAC_PARSE --TMAI_HOSTADDR_STR --TMAI_HOSTNAME_STR -TMAIL_ADDR_FORMATTER -TMAIL_ADDR_MAP_TEST -TMAIL_PRINT -TMAIL_SCAN -TMAIL_STREAM -TMAIL_VERSION +-TMAI_HOSTADDR_STR +-TMAI_HOSTNAME_STR -TMAI_SERVNAME_STR -TMAI_SERVPORT_STR -TMAPS @@ -226,9 +217,9 @@ -TMDB_val -TMILTER -TMILTER8 +-TMILTERS -TMILTER_MACROS -TMILTER_MSG_CONTEXT --TMILTERS -TMIME_ENCODING -TMIME_INFO -TMIME_STACK @@ -246,6 +237,7 @@ -TMOCK_APPL_SIG -TMOCK_APPL_STATUS -TMOCK_EXPECT +-TMOCK_UNIX_SERVER -TMSG_OUTPUT_INFO -TMSG_STATS -TMULTI_SERVER @@ -259,7 +251,6 @@ -TNAME_MASK -TNBBIO -TNVTABLE_INFO --Toff_t -TOPTIONS -TPCF_DBMS_INFO -TPCF_EVAL_CTX @@ -273,7 +264,6 @@ -TPCF_SERVICE_PATTERN -TPCF_STRING_NV -TPEER_NAME --Tpem_load_state_t -TPGSQL_NAME -TPICKUP_INFO -TPIPE_ATTR @@ -281,9 +271,9 @@ -TPIPE_STATE -TPLMYSQL -TPLPGSQL +-TPOSTMAP_KEY_STATE -TPOST_MAIL_FCLOSE_STATE -TPOST_MAIL_STATE --TPOSTMAP_KEY_STATE -TPRIVATE_STR_TABLE -TPSC_CALL_BACK_ENTRY -TPSC_CLIENT_INFO @@ -313,15 +303,11 @@ -TRECIPIENT -TRECIPIENT_LIST -TREC_TYPE_NAME --Tregex_t --Tregmatch_t --TRES_CONTEXT -TRESOLVE_REPLY -TRESPONSE -TREST_TABLE +-TRES_CONTEXT -TRWR_CONTEXT --Tsasl_conn_t --Tsasl_secret_t -TSCACHE -TSCACHE_CLNT -TSCACHE_MULTI @@ -338,19 +324,12 @@ -TSENDER_LOGIN_MATCH -TSERVER_AC -TSESSION --Tsfsistat -TSHARED_PATH --Tsigset_t -TSINGLE_SERVER -TSINK_COMMAND -TSINK_STATE --Tsize_t -TSLMDB -TSMFICTX --TSM_STATE --TSMTP_ADDR --TSMTP_CLI_ATTR --TSMTP_CMD -TSMTPD_CMD -TSMTPD_DEFER -TSMTPD_ENDPT_LOOKUP_INFO @@ -362,6 +341,9 @@ -TSMTPD_STATE -TSMTPD_TOKEN -TSMTPD_XFORWARD_ATTR +-TSMTP_ADDR +-TSMTP_CLI_ATTR +-TSMTP_CMD -TSMTP_ITERATOR -TSMTP_RESP -TSMTP_SASL_AUTH_CACHE @@ -370,13 +352,10 @@ -TSMTP_TLS_POLICY -TSMTP_TLS_SESS -TSMTP_TLS_SITE_POLICY --Tsockaddr +-TSM_STATE -TSOCKADDR_SIZE -TSPAWN_ATTR --Tssize_t -TSSL --Tssl_cipher_stack_t --Tssl_comp_stack_t -TSSL_CTX -TSSL_SESSION -TSTATE @@ -384,20 +363,17 @@ -TSTRING_TABLE -TSYS_EXITS_DETAIL -TTEST_JMP_BUF --Ttime_t --Ttlsa_filter +-TTLSMGR_SCACHE +-TTLSP_STATE -TTLS_APPL_STATE -TTLS_CERTS -TTLS_CLIENT_INIT_PROPS -TTLS_CLIENT_PARAMS -TTLS_CLIENT_START_PROPS --TTLScontext_t -TTLS_DANE --TTLSMGR_SCACHE -TTLS_PKEYS -TTLS_PRNG_SEED_INFO -TTLS_PRNG_SRC --TTLSP_STATE -TTLS_ROLE -TTLS_SCACHE -TTLS_SCACHE_ENTRY @@ -408,12 +384,10 @@ -TTLS_TLSA -TTLS_USAGE -TTLS_VINFO +-TTLScontext_t -TTOK822 -TTRANSPORT_INFO -TTRIGGER_SERVER --Tuint16_t --Tuint32_t --Tuint8_t -TUSER_ATTR -TVBUF -TVSTREAM @@ -423,11 +397,10 @@ -TWATCHDOG -TWATCH_FD -TX509 +-TX509V3_CTX -TX509_EXTENSION -TX509_NAME --Tx509_stack_t -TX509_STORE_CTX --TX509V3_CTX -TXSASL_CLIENT -TXSASL_CLIENT_CREATE_ARGS -TXSASL_CLIENT_IMPL @@ -444,3 +417,31 @@ -TXSASL_SERVER_CREATE_ARGS -TXSASL_SERVER_IMPL -TXSASL_SERVER_IMPL_INFO +-Taddrinfo +-Tbind_props +-Tcipher_probe_t +-Td2i_X509_t +-Tdane_digest +-Tdane_mtype +-Tfilter_ctx +-Tgeneral_name_stack_t +-Tiana_digest +-Toff_t +-Tpem_load_state_t +-Tregex_t +-Tregmatch_t +-Tsasl_conn_t +-Tsasl_secret_t +-Tsfsistat +-Tsigset_t +-Tsize_t +-Tsockaddr +-Tssize_t +-Tssl_cipher_stack_t +-Tssl_comp_stack_t +-Ttime_t +-Ttlsa_filter +-Tuint16_t +-Tuint32_t +-Tuint8_t +-Tx509_stack_t diff --git a/postfix/HISTORY b/postfix/HISTORY index aca90aee1..c99db9294 100644 --- a/postfix/HISTORY +++ b/postfix/HISTORY @@ -26590,10 +26590,42 @@ Apologies for any names omitted. Report by Spil Oss, fix by Viktor Dukhovni. File: tls/tls_server.c. +20220802 + + Documentation: in the aliases(5) manpage, more specific + pointers to the local(8) manpage sections for delivery to + file, command execution, and delivery rights. File: + proto/aliases. + +20220805 + + Feature: "mail_version" attribute in the SMTPD policy + protocol, with the value of the "mail_version" configuration + parameter. This differs from the "compatibility_level" + attribute, because "mail_version" indicates the presence + of new features, while "compatibility_level" concerns changes + in default settings. Files: global/mail_proto.h, + proto/SMTPD_POLICY_README.html, smtpd/smtpd_check.c. + +20220808 + + Documentation: some Debian releases hard-code the search + path for Cyrus SASL application configuration files, + overriding the cyrus_sasl_config_path setting. Viktor + Dukhovni. File: proto/SASL_README.html. + +20220815 + + Updated the postscreen_dnsbl_sites documentation, based + on questions on the postfix-users mailing list. File: + proto/postconf.proto. + Feature: 'ptest' infrastructure for unit tests, and 'pmock' infrastructure to make tests independent of host configuration, network configuration, or DNS. ptest looks like Go test, - while pmock implements a few ideas from Google gmock. + while pmock implements a few ideas from Google gmock. The + PTEST_README file has some information about how Postfix + unit tests work. This changes the Postfix file footprint as follows: diff --git a/postfix/README_FILES/AAAREADME b/postfix/README_FILES/AAAREADME index 9afa3b7d2..c82d99046 100644 --- a/postfix/README_FILES/AAAREADME +++ b/postfix/README_FILES/AAAREADME @@ -84,3 +84,7 @@ OOtthheerr ttooppiiccss * XCLIENT_README: XCLIENT Command * XFORWARD_README: XFORWARD Command +FFoorr mmaaiinnttaaiinneerrss aanndd ccoonnttrriibbuuttoorrss + + * PTEST_README: Writing Postfix unit tests + diff --git a/postfix/README_FILES/PTEST_README b/postfix/README_FILES/PTEST_README new file mode 100644 index 000000000..78d5d486d --- /dev/null +++ b/postfix/README_FILES/PTEST_README @@ -0,0 +1,444 @@ +WWrriittiinngg PPoossttffiixx uunniitt tteessttss + +------------------------------------------------------------------------------- + +OOvveerrvviieeww + +This document covers, Ptest, a simple unit test framework that was introduced +with Postfix version 3.8. It is modeled after Go tests, with primitives such as +ptest_error() and ptest_fatal() that report test failures, and PTEST_RUN() that +supports subtests. + +Ptest is light-weight compared to more powerful framweworks such as Gtest, but +it avoids the need for adding a large Postfix dependency (a dependency that +would not affect Postfix distributors, but developers only). + + * Simple example + + * Testing one function with TEST_CASE data + + * Testing functions with subtests + + * Suggestions for writing tests + + * Ptest API reference + +SSiimmppllee eexxaammppllee + +Simple tests exercise one function under test, one scenario at a time. Each +scenario calls the function under test with good or bad inputs, and verifies +that the function behaves as expected. The code in Postfix mymalloc_test.c file +is a good example. + +After some #include statements, the file goes like this: + + 27 typedef struct PTEST_CASE { + 28 const char *testname; /* Human-readable description + */ + 29 void (*action) (PTEST_CTX *, const struct PTEST_CASE *); + 30 } PTEST_CASE; + 31 + 32 /* Test functions. */ + 33 + 34 static void test_mymalloc_normal(PTEST_CTX *t, const PTEST_CASE *tp) + 35 { + 36 void *ptr; + 37 + 38 ptr = mymalloc(100); + 39 myfree(ptr); + 40 } + 41 + 42 static void test_mymalloc_panic_too_small(PTEST_CTX *t, const + PTEST_CASE *tp) + 43 { + 44 expect_ptest_log_event(t, "panic: mymalloc: requested length 0"); + 45 (void) mymalloc(0); + 46 ptest_fatal(t, "mymalloc(0) returned"); + 47 } + ... // Test functions for myrealloc(), mystrdup(), mymemdup(). + 260 + 261 static const PTEST_CASE ptestcases[] = { + 262 {"mymalloc + myfree normal case", test_mymalloc_normal, + 263 }, + 264 {"mymalloc panic for too small request", + test_mymalloc_panic_too_small, + 265 }, + ... // Test cases for myrealloc(), mystrdup(), mymemdup(). + 306 }; + 307 + 308 #include + +To run the test: + + $ make test_mymalloc + ... compiler output... + LD_LIBRARY_PATH=/path/to/postfix-source/lib ./mymalloc_test + RUN mymalloc + myfree normal case + PASS mymalloc + myfree normal case + RUN mymalloc panic for too small request + PASS mymalloc panic for too small request + ... results for myrealloc(), mystrdup(), mymemdup()... + mymalloc_test: PASS: 22, SKIP: 0, FAIL: 0 + +This simple example already shows several key features of the ptest framework. + + * Each test is implemented as a separate function (test_mymalloc_normal(), + test_mymalloc_panic_too_small(), and so on). + + * The first test verifies 'normal' behavior: it verifies that mymalloc() will + allocate a small amount of memory, and that myfree() will accept the result + from mymalloc(). When the test is run under a memory checker such as + Valgrind, the memory checker will report no memory leak or other error. + + * The second test is more interesting. + + o The test verifies that mymalloc() will call msg_panic() when the + requested amount of memory is too small. But in this test the msg_panic + () call will not terminate the process like it normally would. The + Ptest framework changes the control flow of msg_panic() and msg_fatal() + such that these functions will terminate their test, instead of their + process. + + o The expect_ptest_log_event() call sets up an expectation that msg_panic + () will produce a specific error message; the test would fail if the + expectation remains unsatisfied. + + o The ptest_fatal() call at the end of the second test is not needed; + this call can only be reached if mymalloc() does not call msg_panic(). + But then the expected panic message will not be logged, and the test + will fail anyway. + + * The ptestcases[] table near the end of the example contains for each test + the name and a pointer to function. As we show in a later example, the + ptestcases[] table can also contain test inputs and expectations. + + * The "#include " at the end pulls in the code that iterates + over the ptestcases[] table and logs progress. + + * The test run output shows that the msg_panic() output in the second test is + silenced; only output from unexpected msg_panic() or other unexpected msg + (3) calls would show up in test run output. + +TTeessttiinngg oonnee ffuunnccttiioonn wwiitthh TTEESSTT__CCAASSEE ddaattaa + +Often, we want to test a module that contains only one function. In that case +we can store all the test inputs and expected results in the PTEST_CASE +structure. + +The examples below are taken from the dict_union_test.c file which test the +unionmap implementation in the file. dict_union.c. + +Background: a unionmap creates a union of tables. For example, the lookup table +"unionmap:{inline:{foo=one},inline:{foo=two}}" will return ("one, two", +DICT_STAT_SUCCESS) when queried with foo, and will return (NOTFOUND, +DICT_STAT_SUCCESS) otherwise. + +First, we present the TEST_CASE structure with additional fields for inputs and +expected results. + + 29 #define MAX_PROBE 5 + 30 + 31 struct probe { + 32 const char *query; + 33 const char *want_value; + 34 int want_error; + 35 }; + 36 + 37 typedef struct PTEST_CASE { + 38 const char *testname; + 39 void (*action) (PTEST_CTX *, const struct PTEST_CASE *); + 40 const char *type_name; + 41 const struct probe probes[MAX_PROBE]; + 42 } PTEST_CASE; + +In the PTEST_CASE structure above: + + * The testname and action fields are standard. We have seen these already in + the simple example above. + + * The type_name field will contain the name of the table, for example + unionmap:{static:one,inline:{foo=two}}. + + * The probes field contains a list of (query, expected result value, expected + error code) that will be used to query the unionmap and to verify the + result value and error code. + +Next we show the test data. Every test calls the same test_dict_union() +function with a different unionmap configuration and with a list of queries +with expected results. The implementation of that function follows after the +test data. + + 78 static const PTEST_CASE ptestcases[] = { + 79 { + 80 /* testname */ "successful lookup: static map + inline map", + 81 /* action */ test_dict_union, + 82 /* type_name */ "unionmap:{static:one,inline:{foo=two}}", + 83 /* probes */ { + 84 {"foo", "one,two", DICT_STAT_SUCCESS}, + 85 {"bar", "one", DICT_STAT_SUCCESS}, + 86 }, + 87 }, { + 88 /* testname */ "error propagation: static map + fail map", + 89 /* action */ test_dict_union, + 90 /* type_name */ "unionmap:{static:one,fail:fail}", + 91 /* probes */ { + 92 {"foo", 0, DICT_STAT_ERROR}, + 93 }, + ... + 102 }; + 103 + 104 #include + +Finally, here is the test_dict_union() function that tests the unionmap +implementation with a given configuration and test queries. + + 44 #define STR_OR_NULL(s) ((s) ? (s) : "null") + 45 + 46 static void test_dict_union(PTEST_CTX *t, const struct PTEST_CASE *tp) + 47 { + 48 DICT *dict; + 49 const struct probe *pp; + 50 const char *got_value; + 51 int got_error; + 52 + 53 if ((dict = dict_open(tp->type_name, O_RDONLY, 0)) == 0) + 54 ptest_fatal(t, "dict_open(\"%s\", O_RDONLY, 0) failed: %m", + 55 tp->type_name); + 56 for (pp = tp->probes; pp < tp->probes + MAX_PROBE && pp->query != + 0; pp++) { + 57 got_value = dict_get(dict, pp->query); + 58 got_error = dict->error; + 59 if (got_value == 0 && pp->want_value == 0) + 60 continue; + 61 if (got_value == 0 || pp->want_value == 0) { + 62 ptest_error(t, "dict_get(dict, \"%s\"): got '%s', want + '%s'", + 63 pp->query, STR_OR_NULL(got_value), + 64 STR_OR_NULL(pp->want_value)); + 65 break; + 66 } + 67 if (strcmp(got_value, pp->want_value) != 0) { + 68 ptest_error(t, "dict_get(dict, \"%s\"): got '%s', want + '%s'", + 69 pp->query, got_value, pp->want_value); + 70 } + 71 if (got_error != pp->want_error) + 72 ptest_error(t, "dict_get(dict,\"%s\") error: got %d, want + %d", + 73 pp->query, got_error, pp->want_error); + 74 } + 75 dict_free(dict); + 76 } + +A test run looks like this: + + $ make test_dict_union + ...compiler output... + LD_LIBRARY_PATH=/path/to/postfix-source/lib ./dict_union_test + RUN successful lookup: static map + inline map + PASS successful lookup: static map + inline map + RUN error propagation: static map + fail map + PASS error propagation: static map + fail map + ... + dict_union_test: PASS: 3, SKIP: 0, FAIL: 0 + +TTeessttiinngg ffuunnccttiioonnss wwiitthh ssuubbtteessttss + +Sometimes it is not convenient to store test data in a PTEST_CASE structure. +This can happen when converting an existing test into Ptest, or when the module +under test contains multiple functions that need different kinds of test data. +The solution is to create a _test.c file with the structure shown below. The +example is based on code in map_search_test.c that was converted from an +existing test into Ptest. + + * One PTEST_CASE structure definition without test data. + + 50 typedef struct PTEST_CASE { + 51 const char *testname; + 52 void (*action) (PTEST_CTX *, const struct PTEST_CASE *); + 53 } PTEST_CASE; + + * One test function for each module function that needs to be tested, and one + table with test cases for that module function. In this case there is only + one module function (map_search()) that needs to be tested, so there is + only one test function (test_map_search()). + + 67 #define MAX_WANT_LOG 5 + 68 + 69 static void test_map_search(PTEST_CTX *t, const struct PTEST_CASE + *unused) + 70 { + 71 /* Test cases with inputs and expected outputs. */ + 72 struct test { + 73 const char *map_spec; + 74 int want_return; /* 0=fail, 1=success */ + 75 const char *want_log[MAX_WANT_LOG]; + 76 const char *want_map_type_name; /* 0 or match */ + 77 const char *exp_search_order; /* 0 or match */ + 78 }; + 79 static struct test test_cases[] = { + 80 { /* 0 */ "type", 0, { + 81 "malformed map specification: 'type'", + 82 "expected maptype:mapname instead of 'type'", + 83 }, 0}, + ... // ...other test cases... + 111 }; + + * In a test function, iterate over its table with test cases, using PTEST_RUN + () to run each test case in its own subtest. + + 129 for (tp = test_cases; tp->map_spec; tp++) { + 130 vstring_sprintf(test_label, "test %d", (int) (tp - + test_cases)); + 131 PTEST_RUN(t, STR(test_label), { + 132 for (cpp = tp->want_log; cpp < tp->want_log + MAX_WANT_LOG + && *cpp; cpp++) + 133 expect_ptest_log_event(t, *cpp); + 134 map_search_from_create = map_search_create(tp->map_spec); + ... // ...verify that the result is as expected... + ... // ...use ptest_return() or ptest_fatal() to exit from a + test... + 173 }); + 174 } + ... + 178 } + + * Create a ptestcases[] table to call each test function once, and include + the Ptest main program. + + 183 static const PTEST_CASE ptestcases[] = { + 184 "test_map_search", test_map_search, + 185 }; + 186 + 187 #include + +See the file map_search_test.c for a complete example. + +This is what a test run looks like: + + $ make test_map_search + ...compiler output... + LD_LIBRARY_PATH=/path/to/postfix-source/lib ./map_search_test + RUN test_map_search + RUN test_map_search/test 0 + PASS test_map_search/test 0 + .... + PASS test_map_search + map_search_test: PASS: 13, SKIP: 0, FAIL: 0 + +This shows that the subtest name is appended to the parent test name, formatted +as parent-name/child-name. + +SSuuggggeessttiioonnss ffoorr wwrriittiinngg tteessttss + +Ptest is loosely inspired on Go test, especially its top-level test functions +and its methods T.run(), T.error() and T.fatal(). + +Suggestions for test style may look familiar to Go programmers: + + * Use variables named got_xxx and want_xxx, and when a test result is + unexpected, log the discrepancy as "got , want ". + + * Report discrepancies with ptest_error() if possible; use ptest_fatal() only + when continuing the test would produce nonsensical results. + + * Where it makes sense use a table with testcases and use PTEST_RUN() to run + each testcase in its own subtest. + +Other suggestions: + + * Consider running tests under a memory checker such as Valgrind. Use + ptest_defer() to avoid memory leaks when a test may terminate early. + + * Always test non-error and error cases, to cover all code paths in the + function under test. + +PPtteesstt AAPPII rreeffeerreennccee + + * Managing test errors + + * Managing log events + + * Managing test execution + +MMaannaaggiinngg tteesstt eerrrroorrss + +As one might expect, Ptest has support to flag unexpected test results as +errors. + +vvooiidd pptteesstt__eerrrroorr((PPTTEESSTT__CCTTXX **tt,, ccoonnsstt cchhaarr **ffoorrmmaatt,, ......)) + Called from inside a test to report an unexpected test result, and to flag + the test as failed without terminating the test. This call can be ignored + with expect_ptest_error(). + +vvooiidd pptteesstt__ffaattaall((PPTTEESSTT__CCTTXX **tt,, ccoonnsstt cchhaarr **ffoorrmmaatt,, ......)) + Called from inside a test to report an unexpected test result, to flag the + test as failed, and to terminate the test. This call cannot be ignored with + expect_ptest_error(). +For convenience, Ptest has can also report non-error information. + +vvooiidd pptteesstt__iinnffoo((PPTTEESSTT__CCTTXX **tt,, ccoonnsstt cchhaarr **ffoorrmmaatt,, ......)) + Called from inside a test to report a non-error condition without + terminating the test. This call cannot be ignored with expect_ptest_error + (). +Finally, Ptest has support to test ptest_error() itself, to verify that an +intentional error is reported as expected. + +vvooiidd eexxppeecctt__pptteesstt__eerrrroorr((PPTTEESSTT__CCTTXX **tt,, ccoonnsstt cchhaarr **tteexxtt)) + Called from inside a test to expect exactly one ptest_error() call with the + specified text, and to ignore that ptest_error() call (i.e. don't flag the + test as failed). To ignore multiple calls, call expect_ptest_error() + multiple times. A test is flagged as failed when an expected error is not + reported (and of course when an error is reported that is not expected with + expect_ptest_error()). + +MMaannaaggiinngg lloogg eevveennttss + +Ptest integrates with Postfix msg(3) logging. + + * Ptest changes the control flow of msg_fatal() and msg_panic(). When these + functions are called during a test, Ptest flags a test as failed and + terminates the test instead of the process. + + * Ptest silences the output from msg_info() and other msg(3) calls, and + installs a log event listener tp monitor Postfix logging. + +Ptest provides the following API to manage log events: + +vvooiidd eexxppeecctt__pptteesstt__lloogg__eevveenntt((PPTTEESSTT__CCTTXX **tt,, ccoonnsstt cchhaarr **tteexxtt)) + Called from inside a test to expect exactly one msg(3) call with the + specified text. To expect multiple events, call expect_ptest_log_event() + multiple times. A test is flagged as failed when expected text is not + logged, or when text is logged that is not expected with + expect_ptest_log_event(). + +MMaannaaggiinngg tteesstt eexxeeccuuttiioonn + +Ptest has a number of primitives that control test execution. + +vvooiidd PPTTEESSTT__RRUUNN((PPTTEESSTT__CCTTXX **tt,, ccoonnsstt cchhaarr **tteesstt__nnaammee,, {{ ccooddee iinn bbrraacceess }})) + Called from inside a test to run the { code in braces } in it own subtest + environment. In the test progress report, the subtest name is appended to + the parent test name, formatted as parent-name/child-name. + + NOTE: because PTEST_RUN() is a macro, the { code in braces } must not + contain a return statement; use ptest_return() instead. It is OK for { code + in braces } to call a function that uses return. + +NNOORREETTUURRNN pptteesstt__sskkiipp((PPTTEESSTT__CCTTXX **tt)) + Called from inside a test to flag a test as skipped, and to terminate the + test without terminating the process. Use this to disable tests that are + not applicable for a specific system type or build configuration. + +NNOORREETTUURRNN pptteesstt__rreettuurrnn((PPTTEESSTT__CCTTXX **tt)) + Called from inside a test to terminate the test without terminating the + process. + +vvooiidd pptteesstt__ddeeffeerr((PPTTEESSTT__CCTTXX **tt,, vvooiidd ((**ddeeffeerr__ffnn))((vvooiidd **)),, vvooiidd **ddeeffeerr__ccttxx)) + Called once from inside a test, to call defer_fn(defer_ctx) after the test + completes. This is typically used to eliminate a resource leak in tests + that terminate the test early. + + NOTE: The deferred function is designed to run outside a test, and + therefore it must not call Ptest functions. diff --git a/postfix/README_FILES/SASL_README b/postfix/README_FILES/SASL_README index e5eabc57a..580a0131d 100644 --- a/postfix/README_FILES/SASL_README +++ b/postfix/README_FILES/SASL_README @@ -186,6 +186,13 @@ postfix/sasl/, /var/lib/sasl2/ etc. See the output of postconf cyrus_sasl_config_path and/or the distribution-specific documentation to determine the expected location. +Some Debian-based Postfix distributions patch Postfix to hardcode a non-default +search path, making it impossible to set an alternate search path via the +"cyrus_sasl_config_path" parameter. This is likely to be the case when the +distribution documents a Postfix-specific path (e.g. /etc/postfix/sasl/) that +is different from the default value of "cyrus_sasl_config_path" (which then is +likely to be empty). + NNoottee Cyrus SASL searches /usr/lib/sasl2/ first. If it finds the specified diff --git a/postfix/README_FILES/SMTPD_POLICY_README b/postfix/README_FILES/SMTPD_POLICY_README index 46bbf165f..47a6fa310 100644 --- a/postfix/README_FILES/SMTPD_POLICY_README +++ b/postfix/README_FILES/SMTPD_POLICY_README @@ -87,6 +87,7 @@ a delegated SMTPD access policy request: server_port=54321 PPoossttffiixx vveerrssiioonn 33..88 aanndd llaatteerr:: compatibility_level=major.minor.patch + mail_version=3.8.0 [empty line] Notes: @@ -170,6 +171,10 @@ Notes: parameter value. It has the form major.minor.patch where minor and patch may be absent. + * The "mail_version" attribute corresponds to the mail_version parameter + value. It has the form major.minor.patch for stable releases, and + major.minor-yyyymmdd for unstable releases. + The following is specific to SMTPD delegated policy requests: * Protocol names are ESMTP or SMTP. diff --git a/postfix/TODO b/postfix/TODO index cc67947e1..42943dea1 100644 --- a/postfix/TODO +++ b/postfix/TODO @@ -1,3 +1,59 @@ +Consolidate postscreen_dnsbl tests that differ only in data. + +Add a mock_server_test case for a non-matching request (error propagation +check). + +DONE postscreen_dnsbl_test.c. The code structure is + +void psc_dnsbl_init(void) // one-time initialization + +int psc_dnsbl_request(const char *client_addr, + void (*callback) (int, void *), + void *context) + +Calls LOCAL_CONNECT() which would need to be mocked (this calls +unix_connect() on solaris 9+ and all other systems). Returns a request +index that must be passed to psc_dnsbl_retrieve(). + +psc_dnsbl_retrieve() is called before the pregreet_wait timer expires when +a client hangs up, when postscreen drops the client, or by the callback. +the pregreet_wait timer expires, or by the callback. + +In the following paragraph, a direct call is a call that is not made +through the events framework (for example, using event_request_timer() +with a zero delay). + +The callback is called directly when the complete score for client_addr +is known before the pregreet_wait timer expires. The callback then +directly calls psc_dnsbl_retrieve(). + +Test structure: + +- We need a mock attribute server (in-process or in-child process) that +responds to requests. Or do we? It could be all memory streams that read +from prepared VSTRINGs + +How do we test a program that uses the events framework? + +- Use a socketpair or pipe and write messages smaller than the buffer +size, so that writes would not block (or use non-blocking writes to be +informed when a write is too large). A pipe will accept 16384 +bytes on NetBSD 9.2 and OmniOS 5.11 (Solaris), and 65536 bytes +on FreeBSD 13.0, macOS 11.6.1, and Linux 5.8.15 / Fedora 33. +With socketpairs the results vary. +https://www.netmeister.org/blog/ipcbufs.html + +- Async mock client/server: create pipe; write request, and set up a +read request that will wake up the mock server, compare serialized +request against expectation, write prepared serialized reponse. + + +================ +DONE Split documentation into PTEST_README and PMOCK_README + +PTEST_README: intro, links to all sections, simple example, more complex +example, managing errors, managing logs, managing flows. + TODO write a TEST_README that summarizes how to write simple tests that don't need custom PTEST_CASE fields (use the example in ptest_main.h), how to report test errors and how to require them, @@ -7,8 +63,9 @@ and how to mock out dependencies (use example in pmock_expect_test.c). TODO document NO_MOCK_WRAPPERS in makedefs. -TODO move PCRE tests to src/global, and either make them skippable -(#ifndef USE_DYNAMIC_MAPS) or support dynamic loading in tests. +TODO make PCRE tests skippable (#ifndef USE_DYNAMIC_MAPS), or move +them to src/global and implement support for dynamic loading +in tests. DONE Need a way to SKIP tests, and report those in the summmary. @@ -24,6 +81,8 @@ TODO Port haproxy_srvr.c, to use PTEST. TODO Test that eq_sockaddr() and eq_addrinfo compare all fields for equality. +TODO Add the missing tests in myaddrinfo_test.c. + ============== DONE Subtests. What would the API look like? @@ -56,21 +115,21 @@ In-line code, single-macro alternative: RUN_TEST(t, name, { /* do actual test */ - }); + }); Now we're talking! -Both Linux gcc 11.3.1 and FreeBSD clang version 11.0.1 pre-process {} -inside () without problems. +Both Linux gcc 11.3.1 and FreeBSD clang version 11.0.1 have no +problems with compiling compile code with {} inside a macro argument. indent complains about "Unbalanced parens" before the '(' and "Extra )" before the ')', but formats the code correctly. -clang-format 12.0.1 handles {} inside (), but needs comments with /* -clang-format off */ and /* clang-format on */ to avoid messing up the -macro definition (it removes the '\' at the end of the lines). +clang-format 12.0.1 handles {} inside a macro argument, but needs comments +with /* clang-format off */ and /* clang-format on */ to avoid messing +up the macro definition (it removes the '\' at the end of the lines). -TODO: make sure that a subtest within a subtest propagates +DONE: make sure that a subtest within a subtest propagates the number of subtests passed/failed. Assume that there always is a parent (the root is special). diff --git a/postfix/WISHLIST b/postfix/WISHLIST index 12e8b031e..6738649a9 100644 --- a/postfix/WISHLIST +++ b/postfix/WISHLIST @@ -9,6 +9,8 @@ Wish list: Scan Postfix code with github.com/googleprojectzero/weggli (depends on "rust"). + Migrate masquerade_domains from ARGV to STRING_LIST. + Enforce var_line_limit in util/attr_scan*c. Investigate clang-format compatibility compared to indent. @@ -26,8 +28,6 @@ Wish list: WARN_IF_REJECT like prefix that disables the error counter increment. - Send the Postfix version in a policy server request. - postscreen_dnsbl_sites is evaluated in the reverse order, breaking expectations when different reply patterns have different weights. We need a compatibility_level feature to correct this. diff --git a/postfix/conf/aliases b/postfix/conf/aliases index 941551e9d..8f1a28451 100644 --- a/postfix/conf/aliases +++ b/postfix/conf/aliases @@ -108,16 +108,20 @@ decode: root # with the RFC 822 standard. # # /file/name -# Mail is appended to /file/name. See local(8) for -# details of delivery to file. Delivery is not lim- -# ited to regular files. For example, to dispose of -# unwanted mail, deflect it to /dev/null. +# Mail is appended to /file/name. For details on how +# a file is written see the sections "EXTERNAL FILE +# DELIVERY" and "DELIVERY RIGHTS" in the local(8) +# documentation. Delivery is not limited to regular +# files. For example, to dispose of unwanted mail, +# deflect it to /dev/null. # # |command # Mail is piped into command. Commands that contain # special characters, such as whitespace, should be -# enclosed between double quotes. See local(8) for -# details of delivery to command. +# enclosed between double quotes. For details on how +# a command is executed see "EXTERNAL COMMAND DELIV- +# ERY" and "DELIVERY RIGHTS" in the local(8) documen- +# tation. # # When the command fails, a limited amount of command # output is mailed back to the sender. The file @@ -218,18 +222,17 @@ decode: root # the recipient_delimiter is set to "-". # # recipient_delimiter (empty) -# The set of characters that can separate a user name -# from its extension (example: user+foo), or a .for- -# ward file name from its extension (example: .for- -# ward+foo). +# The set of characters that can separate an email +# address localpart, user name, or a .forward file +# name from its extension. # # Available in Postfix version 2.3 and later: # # frozen_delivered_to (yes) -# Update the local(8) delivery agent's idea of the -# Delivered-To: address (see prepend_deliv- -# ered_header) only once, at the start of a delivery -# attempt; do not update the Delivered-To: address +# Update the local(8) delivery agent's idea of the +# Delivered-To: address (see prepend_deliv- +# ered_header) only once, at the start of a delivery +# attempt; do not update the Delivered-To: address # while expanding aliases or .forward files. # # STANDARDS @@ -242,12 +245,12 @@ decode: root # postconf(5), configuration parameters # # README FILES -# Use "postconf readme_directory" or "postconf html_direc- +# Use "postconf readme_directory" or "postconf html_direc- # tory" to locate this information. # DATABASE_README, Postfix lookup table overview # # LICENSE -# The Secure Mailer license must be distributed with this +# The Secure Mailer license must be distributed with this # software. # # AUTHOR(S) diff --git a/postfix/conf/postfix-files b/postfix/conf/postfix-files index 643a1f319..cda021929 100644 --- a/postfix/conf/postfix-files +++ b/postfix/conf/postfix-files @@ -303,6 +303,7 @@ $readme_directory/MEMCACHE_README:f:root:-:644 $readme_directory/MILTER_README:f:root:-:644 $readme_directory/MULTI_INSTANCE_README:f:root:-:644 $readme_directory/MYSQL_README:f:root:-:644 +$readme_directory/PTEST_README:f:root:-:644 $readme_directory/SMTPUTF8_README:f:root:-:644 $readme_directory/SQLITE_README:f:root:-:644 $readme_directory/NFS_README:f:root:-:644 @@ -364,6 +365,7 @@ $html_directory/MEMCACHE_README.html:f:root:-:644 $html_directory/MILTER_README.html:f:root:-:644 $html_directory/MULTI_INSTANCE_README.html:f:root:-:644 $html_directory/MYSQL_README.html:f:root:-:644 +$html_directory/PTEST_README.html:f:root:-:644 $html_directory/SMTPUTF8_README.html:f:root:-:644 $html_directory/SQLITE_README.html:f:root:-:644 $html_directory/NFS_README.html:f:root:-:644 diff --git a/postfix/html/PTEST_README.html b/postfix/html/PTEST_README.html new file mode 100644 index 000000000..2cdb25224 --- /dev/null +++ b/postfix/html/PTEST_README.html @@ -0,0 +1,599 @@ + + + + + + +Writing Postfix unit tests + + + + + + + +

Writing +Postfix unit tests

+ +
+ +

Overview

+ +

This document covers, Ptest, a simple unit test framework that +was introduced with Postfix version 3.8. It is modeled after Go +tests, with primitives such as ptest_error() and ptest_fatal() that +report test failures, and PTEST_RUN() that supports subtests.

+ +

Ptest is light-weight compared to more powerful framweworks +such as Gtest, but it avoids the need for adding a large Postfix +dependency (a dependency that would not affect Postfix distributors, +but developers only).

+ + + +

Simple example

+ +

Simple tests exercise one function under test, one scenario at +a time. Each scenario calls the function under test with good or +bad inputs, and verifies that the function behaves as expected. The +code in Postfix mymalloc_test.c file is a good example.

+ +

After some #include statements, the file goes like +this:

+ +
+
+ 27 typedef struct PTEST_CASE {
+ 28     const char *testname;               /* Human-readable description */
+ 29     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+ 30 } PTEST_CASE;
+ 31 
+ 32 /* Test functions. */
+ 33 
+ 34 static void test_mymalloc_normal(PTEST_CTX *t, const PTEST_CASE *tp)
+ 35 {
+ 36     void   *ptr;
+ 37 
+ 38     ptr = mymalloc(100);
+ 39     myfree(ptr);
+ 40 }
+ 41 
+ 42 static void test_mymalloc_panic_too_small(PTEST_CTX *t, const PTEST_CASE *tp)
+ 43 {
+ 44     expect_ptest_log_event(t, "panic: mymalloc: requested length 0");
+ 45     (void) mymalloc(0);
+ 46     ptest_fatal(t, "mymalloc(0) returned");
+ 47 }
+...     // Test functions for myrealloc(), mystrdup(), mymemdup().
+260
+261 static const PTEST_CASE ptestcases[] = {
+262     {"mymalloc + myfree normal case", test_mymalloc_normal,
+263     },
+264     {"mymalloc panic for too small request", test_mymalloc_panic_too_small,
+265     },
+...     // Test cases for myrealloc(), mystrdup(), mymemdup().
+306 };
+307 
+308 #include <ptest_main.h>
+
+
+ +

To run the test:

+ +
+
+$ make test_mymalloc
+... compiler output...
+LD_LIBRARY_PATH=/path/to/postfix-source/lib ./mymalloc_test
+RUN  mymalloc + myfree normal case
+PASS mymalloc + myfree normal case
+RUN  mymalloc panic for too small request
+PASS mymalloc panic for too small request
+... results for myrealloc(), mystrdup(), mymemdup()...
+mymalloc_test: PASS: 22, SKIP: 0, FAIL: 0
+
+
+ +

This simple example already shows several key features of the ptest +framework.

+ +
    + +
  • Each test is implemented as a separate function +(test_mymalloc_normal(), test_mymalloc_panic_too_small(), +and so on).

    + +
  • The first test verifies 'normal' behavior: it verifies that +mymalloc() will allocate a small amount of memory, and that +myfree() will accept the result from mymalloc(). +When the test is run under a memory checker such as Valgrind, the +memory checker will report no memory leak or other error.

    + +
  • The second test is more interesting.

    + +
      + +
    • The test verifies that mymalloc() will call +msg_panic() when the requested amount of memory is too +small. But in this test the msg_panic() call will not +terminate the process like it normally would. The Ptest framework +changes the control flow of msg_panic() and msg_fatal() +such that these functions will terminate their test, instead of +their process.

      + +
    • The expect_ptest_log_event() call sets up an +expectation that msg_panic() will produce a specific error +message; the test would fail if the expectation remains unsatisfied. +

      + +
    • The ptest_fatal() call at the end of the second +test is not needed; this call can only be reached if mymalloc() +does not call msg_panic(). But then the expected panic +message will not be logged, and the test will fail anyway.

      + +
    + +
  • The ptestcases[] table near the end of the example +contains for each test the name and a pointer to function. As we +show in a later example, the ptestcases[] table can also +contain test inputs and expectations.

    + +
  • The "#include <ptest_main.h>" at the end pulls +in the code that iterates over the ptestcases[] table and +logs progress. + +

  • The test run output shows that the msg_panic() +output in the second test is silenced; only output from unexpected +msg_panic() or other unexpected msg(3) calls would +show up in test run output.

    + +
+ +

Testing one function with +TEST_CASE data

+ +

Often, we want to test a module that contains only one function. In +that case we can store all the test inputs and expected results in the +PTEST_CASE structure.

+ +

The examples below are taken from the dict_union_test.c +file which test the unionmap implementation in the file. +dict_union.c.

+ +

Background: a unionmap creates a union of tables. For example, +the lookup table "unionmap:{inline:{foo=one},inline:{foo=two}}" +will return ("one, two", DICT_STAT_SUCCESS) when queried +with foo, and will return (NOTFOUND, DICT_STAT_SUCCESS) +otherwise.

+ +

First, we present the TEST_CASE structure with additional fields +for inputs and expected results.

+ +
+
+ 29 #define MAX_PROBE       5
+ 30 
+ 31 struct probe {
+ 32     const char *query;
+ 33     const char *want_value;
+ 34     int     want_error;
+ 35 };
+ 36 
+ 37 typedef struct PTEST_CASE {
+ 38     const char *testname;
+ 39     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+ 40     const char *type_name;
+ 41     const struct probe probes[MAX_PROBE];
+ 42 } PTEST_CASE;
+
+
+ +

In the PTEST_CASE structure above:

+ +
    + +
  • The testname and action fields are +standard. We have seen these already in the simple example above. +

    + +

  • The type_name field will contain the name of the table, +for example unionmap:{static:one,inline:{foo=two}}.

    + +
  • The probes field contains a list of (query, expected +result value, expected error code) that will be used to query the unionmap +and to verify the result value and error code. +

    + +
+ +

Next we show the test data. Every test calls the same +test_dict_union() function with a different unionmap +configuration and with a list of queries with expected results. The +implementation of that function follows after the test data.

+ +
+
+ 78 static const PTEST_CASE ptestcases[] = {
+ 79     {
+ 80          /* testname */ "successful lookup: static map + inline map",
+ 81          /* action */ test_dict_union,
+ 82          /* type_name */ "unionmap:{static:one,inline:{foo=two}}",
+ 83          /* probes */ {
+ 84             {"foo", "one,two", DICT_STAT_SUCCESS},
+ 85             {"bar", "one", DICT_STAT_SUCCESS},
+ 86         },
+ 87     }, {
+ 88          /* testname */ "error propagation: static map + fail map",
+ 89          /* action */ test_dict_union,
+ 90          /* type_name */ "unionmap:{static:one,fail:fail}",
+ 91          /* probes */ {
+ 92             {"foo", 0, DICT_STAT_ERROR},
+ 93         },
+...
+102 };
+103 
+104 #include <ptest_main.h>
+
+
+ +

Finally, here is the test_dict_union() function that +tests the unionmap implementation with a given configuration +and test queries.

+ +
+
+ 44 #define STR_OR_NULL(s)  ((s) ? (s) : "null")
+ 45 
+ 46 static void test_dict_union(PTEST_CTX *t, const struct PTEST_CASE *tp)
+ 47 {
+ 48     DICT   *dict;
+ 49     const struct probe *pp;
+ 50     const char *got_value;
+ 51     int     got_error;
+ 52 
+ 53     if ((dict = dict_open(tp->type_name, O_RDONLY, 0)) == 0)
+ 54         ptest_fatal(t, "dict_open(\"%s\", O_RDONLY, 0) failed: %m",
+ 55                     tp->type_name);
+ 56     for (pp = tp->probes; pp < tp->probes + MAX_PROBE && pp->query != 0; pp++) {
+ 57         got_value = dict_get(dict, pp->query);
+ 58         got_error = dict->error;
+ 59         if (got_value == 0 && pp->want_value == 0)
+ 60             continue;
+ 61         if (got_value == 0 || pp->want_value == 0) {
+ 62             ptest_error(t, "dict_get(dict, \"%s\"): got '%s', want '%s'",
+ 63                         pp->query, STR_OR_NULL(got_value),
+ 64                         STR_OR_NULL(pp->want_value));
+ 65             break;
+ 66         }
+ 67         if (strcmp(got_value, pp->want_value) != 0) {
+ 68             ptest_error(t, "dict_get(dict, \"%s\"): got '%s', want '%s'",
+ 69                         pp->query, got_value, pp->want_value);
+ 70         }
+ 71         if (got_error != pp->want_error)
+ 72             ptest_error(t, "dict_get(dict,\"%s\") error: got %d, want %d",
+ 73                         pp->query, got_error, pp->want_error);
+ 74     }
+ 75     dict_free(dict);
+ 76 }
+
+
+ +

A test run looks like this:

+ +
+
+$ make test_dict_union
+...compiler output...
+LD_LIBRARY_PATH=/path/to/postfix-source/lib ./dict_union_test
+RUN  successful lookup: static map + inline map
+PASS successful lookup: static map + inline map
+RUN  error propagation: static map + fail map
+PASS error propagation: static map + fail map
+...
+dict_union_test: PASS: 3, SKIP: 0, FAIL: 0
+
+
+ +

Testing functions with subtests

+ +

Sometimes it is not convenient to store test data in a PTEST_CASE +structure. This can happen when converting an existing test into +Ptest, or when the module under test contains multiple functions +that need different kinds of test data. The solution is to create +a _test.c file with the structure shown below. The example +is based on code in map_search_test.c that was converted +from an existing test into Ptest.

+ +
    + +
  • One PTEST_CASE structure definition without test data.

    + +
    + 50 typedef struct PTEST_CASE {
    + 51     const char *testname;
    + 52     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
    + 53 } PTEST_CASE;
    +
    + +
  • One test function for each module function that needs to +be tested, and one table with test cases for that module function. +In this case there is only one module function (map_search()) +that needs to be tested, so there is only one test function +(test_map_search()).

    + +
    + 67 #define MAX_WANT_LOG    5
    + 68 
    + 69 static void test_map_search(PTEST_CTX *t, const struct PTEST_CASE *unused)
    + 70 {
    + 71     /* Test cases with inputs and expected outputs. */
    + 72     struct test {
    + 73         const char *map_spec;
    + 74         int     want_return;            /* 0=fail, 1=success */
    + 75         const char *want_log[MAX_WANT_LOG];
    + 76         const char *want_map_type_name; /* 0 or match */
    + 77         const char *exp_search_order;   /* 0 or match */
    + 78     };
    + 79     static struct test test_cases[] = {
    + 80         { /* 0 */ "type", 0, {
    + 81                 "malformed map specification: 'type'",
    + 82                 "expected maptype:mapname instead of 'type'",
    + 83         }, 0},
    +...        // ...other test cases...
    +111     };
    +
    + +
  • In a test function, iterate over its table with test cases, +using PTEST_RUN() to run each test case in its own subtest. +

    + +
    +129     for (tp = test_cases; tp->map_spec; tp++) {
    +130         vstring_sprintf(test_label, "test %d", (int) (tp - test_cases));
    +131         PTEST_RUN(t, STR(test_label), {
    +132             for (cpp = tp->want_log; cpp < tp->want_log + MAX_WANT_LOG && *cpp; cpp++)
    +133                 expect_ptest_log_event(t, *cpp);
    +134             map_search_from_create = map_search_create(tp->map_spec);
    +...            // ...verify that the result is as expected...
    +...            // ...use ptest_return() or ptest_fatal() to exit from a test...
    +173         });
    +174     }
    +...
    +178 }
    +
    + +
  • Create a ptestcases[] table to call each test +function once, and include the Ptest main program.

    + +
    +183 static const PTEST_CASE ptestcases[] = {
    +184     "test_map_search", test_map_search,
    +185 };
    +186 
    +187 #include <ptest_main.h>
    +
    + +
+ +

See the file map_search_test.c for a complete example. +

+ +

This is what a test run looks like:

+ +
+
+$ make test_map_search
+...compiler output...
+LD_LIBRARY_PATH=/path/to/postfix-source/lib  ./map_search_test
+RUN  test_map_search
+RUN  test_map_search/test 0
+PASS test_map_search/test 0
+....
+PASS test_map_search
+map_search_test: PASS: 13, SKIP: 0, FAIL: 0
+
+
+ +

This shows that the subtest name is appended to the parent test +name, formatted as parent-name/child-name.

+ +

Suggestions for writing tests

+ +

Ptest is loosely inspired on Go test, especially its top-level +test functions and its methods T.run(), T.error() +and T.fatal().

+ +

Suggestions for test style may look familiar to Go programmers: +

+ +
    + +
  • Use variables named got_xxx and want_xxx, +and when a test result is unexpected, log the discrepancy as "got +<what you got>, want <what you want>".

    + +
  • Report discrepancies with ptest_error() if possible; +use ptest_fatal() only when continuing the test would +produce nonsensical results.

    + +
  • Where it makes sense use a table with testcases and use +PTEST_RUN() to run each testcase in its own subtest.

    + +
+ +

Other suggestions:

+ +
    + +
  • Consider running tests under a memory checker such as +Valgrind. Use ptest_defer() to avoid memory leaks when a +test may terminate early.

    + +
  • Always test non-error and error cases, to cover all code +paths in the function under test.

    + +
+ +

Ptest API reference

+ + + +

Managing test errors

+ +

As one might expect, Ptest has support to flag unexpected test +results as errors.

+ +
+ +
void ptest_error(PTEST_CTX *t, +const char *format, ...)
+ +
Called from inside a test to report an unexpected test result, +and to flag the test as failed without terminating the test. This +call can be ignored with expect_ptest_error().

+ +
void ptest_fatal(PTEST_CTX *t, +const char *format, ...)
+ +
Called from inside a test to report an unexpected test result, +to flag the test as failed, and to terminate the test. This call +cannot be ignored with expect_ptest_error().
+ +
+ +

For convenience, Ptest has can also report non-error information. +

+ +
+ +
void ptest_info(PTEST_CTX *t, +const char *format, ...)
+ +
Called from inside a test to report a non-error condition +without terminating the test. This call cannot be ignored with +expect_ptest_error().
+ +
+ +

Finally, Ptest has support to test ptest_error() itself, +to verify that an intentional error is reported as expected.

+ +
+ +
void +expect_ptest_error(PTEST_CTX *t, const char *text) +
+ +
Called from inside a test to expect exactly one ptest_error() +call with the specified text, and to ignore that ptest_error() +call (i.e. don't flag the test as failed). To ignore multiple calls, +call expect_ptest_error() multiple times. A test is flagged +as failed when an expected error is not reported (and of course +when an error is reported that is not expected with +expect_ptest_error()).
+ +
+ +

Managing log events

+ +

Ptest integrates with Postfix msg(3) logging.

+ +
    + +
  • Ptest changes the control flow of msg_fatal() and +msg_panic(). When these functions are called during a test, +Ptest flags a test as failed and terminates the test instead of the +process.

    + +
  • Ptest silences the output from msg_info() and +other msg(3) calls, and installs a log event listener tp +monitor Postfix logging.

    + +
+ +

Ptest provides the following API to manage log events:

+ +
+ +
void +expect_ptest_log_event(PTEST_CTX *t, const char *text) +
+ +
Called from inside a test to expect exactly one msg(3) +call with the specified text. To expect multiple events, call +expect_ptest_log_event() multiple times. A test is flagged +as failed when expected text is not logged, or when text is logged +that is not expected with expect_ptest_log_event().
+ +
+ +

Managing test execution

+ +

Ptest has a number of primitives that control test execution. +

+ +
+ +
void PTEST_RUN(PTEST_CTX *t, const +char *test_name, { code in braces })
+ +
Called from inside a test to run the { code in braces +} in it own subtest environment. In the test progress report, +the subtest name is appended to the parent test name, formatted as +parent-name/child-name.

NOTE: because PTEST_RUN() + is a macro, the { code in braces } must not contain +a return statement; use ptest_return() instead. +It is OK for { code in braces } to call a function that +uses return.

+ +
NORETURN ptest_skip(PTEST_CTX +*t)
+ +
Called from inside a test to flag a test as skipped, and to +terminate the test without terminating the process. Use this to +disable tests that are not applicable for a specific system type +or build configuration.

+ +
NORETURN ptest_return(PTEST_CTX +*t)
+ +
Called from inside a test to terminate the test without +terminating the process.

+ +
void ptest_defer(PTEST_CTX *t, +void (*defer_fn)(void *), void *defer_ctx)
+ +
Called once from inside a test, to call defer_fn(defer_ctx) +after the test completes. This is typically used to eliminate a +resource leak in tests that terminate the test early.

+NOTE: The deferred function is designed to run outside a test, and +therefore it must not call Ptest functions.
+ +
+ + + + + diff --git a/postfix/html/SASL_README.html b/postfix/html/SASL_README.html index 6520a6966..f31391668 100644 --- a/postfix/html/SASL_README.html +++ b/postfix/html/SASL_README.html @@ -280,6 +280,14 @@ configuration file in /etc/postfix/sasl/, cyrus_sasl_config_path and/or the distribution-specific documentation to determine the expected location.

+
  • Some Debian-based Postfix distributions patch Postfix to +hardcode a non-default search path, making it impossible to set an +alternate search path via the "cyrus_sasl_config_path" parameter. This +is likely to be the case when the distribution documents a +Postfix-specific path (e.g. /etc/postfix/sasl/) that is +different from the default value of "cyrus_sasl_config_path" (which +then is likely to be empty).

  • +
    diff --git a/postfix/html/SMTPD_POLICY_README.html b/postfix/html/SMTPD_POLICY_README.html index aaa5218ed..ba73f0058 100644 --- a/postfix/html/SMTPD_POLICY_README.html +++ b/postfix/html/SMTPD_POLICY_README.html @@ -118,6 +118,7 @@ server_address=10.3.2.1 server_port=54321 Postfix version 3.8 and later: compatibility_level=major.minor.patch +mail_version=3.8.0 [empty line]
    @@ -220,6 +221,12 @@ server_port=54321 major.minor.patch where minor and patch may be absent.

    +
  • The "mail_version" attribute corresponds to the + mail_version parameter value. It has the form + major.minor.patch for stable releases, and + major.minor-yyyymmdd for unstable releases. +

    +

    The following is specific to SMTPD delegated policy requests: diff --git a/postfix/html/aliases.5.html b/postfix/html/aliases.5.html index e7d5b663b..68aa58a51 100644 --- a/postfix/html/aliases.5.html +++ b/postfix/html/aliases.5.html @@ -67,38 +67,41 @@ ALIASES(5) ALIASES(5) 822 standard. /file/name - Mail is appended to /file/name. See local(8) for details of - delivery to file. Delivery is not limited to regular files. - For example, to dispose of unwanted mail, deflect it to - /dev/null. + Mail is appended to /file/name. For details on how a file is + written see the sections "EXTERNAL FILE DELIVERY" and "DELIVERY + RIGHTS" in the local(8) documentation. Delivery is not limited + to regular files. For example, to dispose of unwanted mail, + deflect it to /dev/null. |command - Mail is piped into command. Commands that contain special char- - acters, such as whitespace, should be enclosed between double - quotes. See local(8) for details of delivery to command. - - When the command fails, a limited amount of command output is - mailed back to the sender. The file /usr/include/sysexits.h - defines the expected exit status codes. For example, use "|exit - 67" to simulate a "user unknown" error, and "|exit 0" to imple- + Mail is piped into command. Commands that contain special char- + acters, such as whitespace, should be enclosed between double + quotes. For details on how a command is executed see "EXTERNAL + COMMAND DELIVERY" and "DELIVERY RIGHTS" in the local(8) documen- + tation. + + When the command fails, a limited amount of command output is + mailed back to the sender. The file /usr/include/sysexits.h + defines the expected exit status codes. For example, use "|exit + 67" to simulate a "user unknown" error, and "|exit 0" to imple- ment an expensive black hole. :include:/file/name - Mail is sent to the destinations listed in the named file. - Lines in :include: files have the same syntax as the right-hand + Mail is sent to the destinations listed in the named file. + Lines in :include: files have the same syntax as the right-hand side of alias entries. - A destination can be any destination that is described in this - manual page. However, delivery to "|command" and /file/name is - disallowed by default. To enable, edit the allow_mail_to_com- + A destination can be any destination that is described in this + manual page. However, delivery to "|command" and /file/name is + disallowed by default. To enable, edit the allow_mail_to_com- mands and allow_mail_to_files configuration parameters. ADDRESS EXTENSION - When alias database search fails, and the recipient localpart contains - the optional recipient delimiter (e.g., user+foo), the search is + When alias database search fails, and the recipient localpart contains + the optional recipient delimiter (e.g., user+foo), the search is repeated for the unextended address (e.g., user). - The propagate_unmatched_extensions parameter controls whether an + The propagate_unmatched_extensions parameter controls whether an unmatched address extension (+foo) is propagated to the result of table lookup. @@ -107,9 +110,9 @@ ALIASES(5) ALIASES(5) before database lookup. REGULAR EXPRESSION TABLES - This section describes how the table lookups change when the table is - given in the form of regular expressions. For a description of regular - expression lookup table syntax, see regexp_table(5) or pcre_table(5). + This section describes how the table lookups change when the table is + given in the form of regular expressions. For a description of regular + expression lookup table syntax, see regexp_table(5) or pcre_table(5). NOTE: these formats do not use ":" at the end of a pattern. Each regular expression is applied to the entire search string. Thus, a @@ -122,21 +125,21 @@ ALIASES(5) ALIASES(5) reasons there is no support for $1, $2 etc. substring interpolation. SECURITY - The local(8) delivery agent disallows regular expression substitution + The local(8) delivery agent disallows regular expression substitution of $1 etc. in alias_maps, because that would open a security hole. - The local(8) delivery agent will silently ignore requests to use the - proxymap(8) server within alias_maps. Instead it will open the table + The local(8) delivery agent will silently ignore requests to use the + proxymap(8) server within alias_maps. Instead it will open the table directly. Before Postfix version 2.2, the local(8) delivery agent will terminate with a fatal error. CONFIGURATION PARAMETERS - The following main.cf parameters are especially relevant. The text - below provides only a parameter summary. See postconf(5) for more + The following main.cf parameters are especially relevant. The text + below provides only a parameter summary. See postconf(5) for more details including examples. alias_database (see 'postconf -d' output) - The alias databases for local(8) delivery that are updated with + The alias databases for local(8) delivery that are updated with "newaliases" or with "sendmail -bi". alias_maps (see 'postconf -d' output) @@ -149,30 +152,30 @@ ALIASES(5) ALIASES(5) Restrict local(8) mail delivery to external files. expand_owner_alias (no) - When delivering to an alias "aliasname" that has an + When delivering to an alias "aliasname" that has an "owner-aliasname" companion alias, set the envelope sender address to the expansion of the "owner-aliasname" alias. propagate_unmatched_extensions (canonical, virtual) - What address lookup tables copy an address extension from the + What address lookup tables copy an address extension from the lookup key to the lookup result. owner_request_special (yes) - Enable special treatment for owner-listname entries in the + Enable special treatment for owner-listname entries in the aliases(5) file, and don't split owner-listname and list- - name-request address localparts when the recipient_delimiter is + name-request address localparts when the recipient_delimiter is set to "-". recipient_delimiter (empty) - The set of characters that can separate an email address local- + The set of characters that can separate an email address local- part, user name, or a .forward file name from its extension. Available in Postfix version 2.3 and later: frozen_delivered_to (yes) - Update the local(8) delivery agent's idea of the Delivered-To: - address (see prepend_delivered_header) only once, at the start - of a delivery attempt; do not update the Delivered-To: address + Update the local(8) delivery agent's idea of the Delivered-To: + address (see prepend_delivered_header) only once, at the start + of a delivery attempt; do not update the Delivered-To: address while expanding aliases or .forward files. STANDARDS diff --git a/postfix/html/index.html b/postfix/html/index.html index 2fd0c1dc2..da6306ef0 100644 --- a/postfix/html/index.html +++ b/postfix/html/index.html @@ -212,6 +212,14 @@ Recipients +

    For maintainers and contributors

    + + + diff --git a/postfix/html/postconf.5.html b/postfix/html/postconf.5.html index 4d9b705fe..03f0ddba9 100644 --- a/postfix/html/postconf.5.html +++ b/postfix/html/postconf.5.html @@ -8609,13 +8609,16 @@ the file is read).

    postscreen_dnsbl_sites (default: empty)
    -

    Optional list of DNS allow/denylist domains, filters and weight +

    Optional list of patterns with DNS allow/denylist domains, filters +and weight factors. When the list is non-empty, the dnsblog(8) daemon will -query these domains with the IP addresses of remote SMTP clients, +query these domains with the reversed IP addresses of remote SMTP +clients, and postscreen(8) will update an SMTP client's DNSBL score with -each non-error reply.

    +each non-error reply as described below.

    -

    Caution: when postscreen rejects mail, it replies with the DNSBL +

    Caution: when postscreen rejects mail, its SMTP response contains +the DNSBL domain name. Use the postscreen_dnsbl_reply_map feature to hide "password" information in DNSBL domain names.

    @@ -8623,26 +8626,25 @@ domain name. Use the postsc specified with postscreen_dnsbl_threshold, postscreen(8) can drop the connection with the remote SMTP client.

    -

    Specify a list of domain=filter*weight entries, separated by +

    Specify a list of domain=filter*weight patterns, separated by comma or whitespace.

      -
    • When no "=filter" is specified, postscreen(8) will use any -non-error DNSBL reply. Otherwise, postscreen(8) uses only DNSBL -replies that match the filter. The filter has the form d.d.d.d, +

    • When a pattern specifies no "=filter", postscreen(8) will +use any non-error DNSBL query result. Otherwise, postscreen(8) +will use only DNSBL +query results that match the filter. The filter has the form d.d.d.d, where each d is a number, or a pattern inside [] that contains one or more ";"-separated numbers or number..number ranges.

      -
    • When no "*weight" is specified, postscreen(8) increments -the remote SMTP client's DNSBL score by 1. Otherwise, the weight must be -an integral number, and postscreen(8) adds the specified weight to -the remote SMTP client's DNSBL score. Specify a negative number for -allowlisting.

      +
    • When a pattern specifies no "*weight", the weight of the +pattern is 1. Otherwise, the weight must be an integral number. +Specify a negative number for allowlisting.

      -
    • When one postscreen_dnsbl_sites entry produces multiple -DNSBL responses, postscreen(8) applies the weight at most once. -

      +
    • When a pattern matches one or more DNSBL query results, +postscreen(8) adds that pattern's weight once to the remote SMTP +client's DNSBL score.

    diff --git a/postfix/man/man5/aliases.5 b/postfix/man/man5/aliases.5 index 628b5d75d..a5da9069e 100644 --- a/postfix/man/man5/aliases.5 +++ b/postfix/man/man5/aliases.5 @@ -71,14 +71,17 @@ The \fIvalue\fR contains one or more of the following: Mail is forwarded to \fIaddress\fR, which is compatible with the RFC 822 standard. .IP \fI/file/name\fR -Mail is appended to \fI/file/name\fR. See \fBlocal\fR(8) -for details of delivery to file. +Mail is appended to \fI/file/name\fR. For details on how a +file is written see the sections "EXTERNAL FILE DELIVERY" +and "DELIVERY RIGHTS" in the \fBlocal\fR(8) documentation. Delivery is not limited to regular files. For example, to dispose of unwanted mail, deflect it to \fB/dev/null\fR. .IP "|\fIcommand\fR" -Mail is piped into \fIcommand\fR. Commands that contain special -characters, such as whitespace, should be enclosed between double -quotes. See \fBlocal\fR(8) for details of delivery to command. +Mail is piped into \fIcommand\fR. Commands that contain +special characters, such as whitespace, should be enclosed +between double quotes. For details on how a command is +executed see "EXTERNAL COMMAND DELIVERY" and "DELIVERY +RIGHTS" in the \fBlocal\fR(8) documentation. .sp When the command fails, a limited amount of command output is mailed back to the sender. The file \fB/usr/include/sysexits.h\fR diff --git a/postfix/man/man5/postconf.5 b/postfix/man/man5/postconf.5 index 1bdbc9cef..289f0de88 100644 --- a/postfix/man/man5/postconf.5 +++ b/postfix/man/man5/postconf.5 @@ -5441,13 +5441,16 @@ Example: .PP This feature is available in Postfix 2.8. .SH postscreen_dnsbl_sites (default: empty) -Optional list of DNS allow/denylist domains, filters and weight +Optional list of patterns with DNS allow/denylist domains, filters +and weight factors. When the list is non\-empty, the \fBdnsblog\fR(8) daemon will -query these domains with the IP addresses of remote SMTP clients, +query these domains with the reversed IP addresses of remote SMTP +clients, and \fBpostscreen\fR(8) will update an SMTP client's DNSBL score with -each non\-error reply. +each non\-error reply as described below. .PP -Caution: when postscreen rejects mail, it replies with the DNSBL +Caution: when postscreen rejects mail, its SMTP response contains +the DNSBL domain name. Use the postscreen_dnsbl_reply_map feature to hide "password" information in DNSBL domain names. .PP @@ -5455,23 +5458,23 @@ When a client's score is equal to or greater than the threshold specified with postscreen_dnsbl_threshold, \fBpostscreen\fR(8) can drop the connection with the remote SMTP client. .PP -Specify a list of domain=filter*weight entries, separated by +Specify a list of domain=filter*weight patterns, separated by comma or whitespace. .IP \(bu -When no "=filter" is specified, \fBpostscreen\fR(8) will use any -non\-error DNSBL reply. Otherwise, \fBpostscreen\fR(8) uses only DNSBL -replies that match the filter. The filter has the form d.d.d.d, +When a pattern specifies no "=filter", \fBpostscreen\fR(8) will +use any non\-error DNSBL query result. Otherwise, \fBpostscreen\fR(8) +will use only DNSBL +query results that match the filter. The filter has the form d.d.d.d, where each d is a number, or a pattern inside [] that contains one or more ";"\-separated numbers or number..number ranges. .IP \(bu -When no "*weight" is specified, \fBpostscreen\fR(8) increments -the remote SMTP client's DNSBL score by 1. Otherwise, the weight must be -an integral number, and \fBpostscreen\fR(8) adds the specified weight to -the remote SMTP client's DNSBL score. Specify a negative number for -allowlisting. +When a pattern specifies no "*weight", the weight of the +pattern is 1. Otherwise, the weight must be an integral number. +Specify a negative number for allowlisting. .IP \(bu -When one postscreen_dnsbl_sites entry produces multiple -DNSBL responses, \fBpostscreen\fR(8) applies the weight at most once. +When a pattern matches one or more DNSBL query results, +\fBpostscreen\fR(8) adds that pattern's weight once to the remote SMTP +client's DNSBL score. .br .PP Examples: diff --git a/postfix/mantools/postlink b/postfix/mantools/postlink index 2b1ad8b2a..6f262a5da 100755 --- a/postfix/mantools/postlink +++ b/postfix/mantools/postlink @@ -1171,6 +1171,18 @@ while (<>) { s/(ftp:\/\/[^ ,"\(\)]*[^ ,"\(\):;!?.])/$1<\/a>/; s/\bRFC\s*([1-9]\d*)/$&<\/a>/g; + # Ptest hyperlinks + + s;\bptest_error\b;$&;g; + s;\bptest_fatal\b;$&;g; + s;\bptest_info\b;$&;g; + s;\bexpect_ptest_error\b;$&;g; + s;\bexpect_ptest_log_event\b;$&;g; + s;\bPTEST_RUN\b;$&;g; + s;\bptest_skip\b;$&;g; + s;\bptest_return\b;$&;g; + s;\bptest_defer\b;$&;g; + # Split README/RFC/parameter/restriction hyperlinks that span line breaks s/()([-A-Za-z0-9_]*)\b([-<\/bB>]*\n *[]*)\b([-A-Za-z0-9_]*)(<\/a>)/$1$2$5$3$1$4$5/; diff --git a/postfix/proto/Makefile.in b/postfix/proto/Makefile.in index 511bd4448..f44a1e39f 100644 --- a/postfix/proto/Makefile.in +++ b/postfix/proto/Makefile.in @@ -37,6 +37,7 @@ HTML = ../html/ADDRESS_CLASS_README.html \ ../html/PGSQL_README.html \ ../html/POSTSCREEN_3_5_README.html \ ../html/POSTSCREEN_README.html \ + ../html/PTEST_README.html \ ../html/QSHAPE_README.html \ ../html/RESTRICTION_CLASS_README.html ../html/SASL_README.html \ ../html/SCHEDULER_README.html ../html/SMTPD_ACCESS_README.html \ @@ -85,6 +86,7 @@ README = ../README_FILES/ADDRESS_CLASS_README \ ../README_FILES/PGSQL_README \ ../README_FILES/POSTSCREEN_3_5_README \ ../README_FILES/POSTSCREEN_README \ + ../README_FILES/PTEST_README \ ../README_FILES/QSHAPE_README \ ../README_FILES/RESTRICTION_CLASS_README \ ../README_FILES/SASL_README ../README_FILES/SCHEDULER_README \ @@ -267,6 +269,9 @@ clobber: ../html/POSTSCREEN_README.html: POSTSCREEN_README.html $(DETAB) $? | $(POSTLINK) >$@ +../html/PTEST_README.html: PTEST_README.html + $(DETAB) $? | $(POSTLINK) >$@ + ../html/QMQP_README.html: QMQP_README.html $(DETAB) $? | $(POSTLINK) >$@ @@ -447,6 +452,9 @@ clobber: ../README_FILES/POSTSCREEN_README: POSTSCREEN_README.html $(DETAB) $? | $(HT2READ) >$@ +../README_FILES/PTEST_README: PTEST_README.html + $(DETAB) $? | $(HT2READ) >$@ + ../README_FILES/QMQP_README: QMQP_README.html $(DETAB) $? | $(HT2READ) >$@ diff --git a/postfix/proto/PTEST_README.html b/postfix/proto/PTEST_README.html new file mode 100644 index 000000000..56323def7 --- /dev/null +++ b/postfix/proto/PTEST_README.html @@ -0,0 +1,599 @@ + + + + + + +Writing Postfix unit tests + + + + + + + +

    Writing +Postfix unit tests

    + +
    + +

    Overview

    + +

    This document covers Ptest, a simple unit test framework that +was introduced with Postfix version 3.8. It is modeled after Go +tests, with primitives such as ptest_error() and ptest_fatal() that +report test failures, and PTEST_RUN() that supports subtests.

    + +

    Ptest is light-weight compared to more powerful frameworks +such as Gtest, but it avoids the need for adding a large Postfix +dependency (a dependency that would not affect Postfix distributors, +but developers only).

    + +
    + +

    Simple example

    + +

    Simple tests exercise one function under test, one scenario at +a time. Each scenario calls the function under test with good or +bad inputs, and verifies that the function behaves as expected. The +code in Postfix mymalloc_test.c file is a good example.

    + +

    After some #include statements, the file goes like +this:

    + +
    +
    + 27 typedef struct PTEST_CASE {
    + 28     const char *testname;               /* Human-readable description */
    + 29     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
    + 30 } PTEST_CASE;
    + 31 
    + 32 /* Test functions. */
    + 33 
    + 34 static void test_mymalloc_normal(PTEST_CTX *t, const PTEST_CASE *tp)
    + 35 {
    + 36     void   *ptr;
    + 37 
    + 38     ptr = mymalloc(100);
    + 39     myfree(ptr);
    + 40 }
    + 41 
    + 42 static void test_mymalloc_panic_too_small(PTEST_CTX *t, const PTEST_CASE *tp)
    + 43 {
    + 44     expect_ptest_log_event(t, "panic: mymalloc: requested length 0");
    + 45     (void) mymalloc(0);
    + 46     ptest_fatal(t, "mymalloc(0) returned");
    + 47 }
    +...     // Test functions for myrealloc(), mystrdup(), mymemdup().
    +260
    +261 static const PTEST_CASE ptestcases[] = {
    +262     {"mymalloc + myfree normal case", test_mymalloc_normal,
    +263     },
    +264     {"mymalloc panic for too small request", test_mymalloc_panic_too_small,
    +265     },
    +...     // Test cases for myrealloc(), mystrdup(), mymemdup().
    +306 };
    +307 
    +308 #include <ptest_main.h>
    +
    +
    + +

    To run the test:

    + +
    +
    +$ make test_mymalloc
    +... compiler output...
    +LD_LIBRARY_PATH=/path/to/postfix-source/lib ./mymalloc_test
    +RUN  mymalloc + myfree normal case
    +PASS mymalloc + myfree normal case
    +RUN  mymalloc panic for too small request
    +PASS mymalloc panic for too small request
    +... results for myrealloc(), mystrdup(), mymemdup()...
    +mymalloc_test: PASS: 22, SKIP: 0, FAIL: 0
    +
    +
    + +

    This simple example already shows several key features of the ptest +framework.

    + +
      + +
    • Each test is implemented as a separate function +(test_mymalloc_normal(), test_mymalloc_panic_too_small(), +and so on).

      + +
    • The first test verifies 'normal' behavior: it verifies that +mymalloc() will allocate a small amount of memory, and that +myfree() will accept the result from mymalloc(). +When the test is run under a memory checker such as Valgrind, the +memory checker will report no memory leak or other error.

      + +
    • The second test is more interesting.

      + +
        + +
      • The test verifies that mymalloc() will call +msg_panic() when the requested amount of memory is too +small. But in this test the msg_panic() call will not +terminate the process like it normally would. The Ptest framework +changes the control flow of msg_panic() and msg_fatal() +such that these functions will terminate their test, instead of +their process.

        + +
      • The expect_ptest_log_event() call sets up an +expectation that msg_panic() will produce a specific error +message; the test would fail if the expectation remains unsatisfied. +

        + +
      • The ptest_fatal() call at the end of the second +test is not needed; this call can only be reached if mymalloc() +does not call msg_panic(). But then the expected panic +message will not be logged, and the test will fail anyway.

        + +
      + +
    • The ptestcases[] table near the end of the example +contains for each test the name and a pointer to function. As we +show in a later example, the ptestcases[] table can also +contain test inputs and expectations.

      + +
    • The "#include <ptest_main.h>" at the end pulls +in the code that iterates over the ptestcases[] table and +logs progress. + +

    • The test run output shows that the msg_panic() +output in the second test is silenced; only output from unexpected +msg_panic() or other unexpected msg(3) calls would +show up in test run output.

      + +
    + +

    Testing one function with +TEST_CASE data

    + +

    Often, we want to test a module that contains only one function. In +that case we can store all the test inputs and expected results in the +PTEST_CASE structure.

    + +

    The examples below are taken from the dict_union_test.c +file which test the unionmap implementation in the file. +dict_union.c.

    + +

    Background: a unionmap creates a union of tables. For example, +the lookup table "unionmap:{inline:{foo=one},inline:{foo=two}}" +will return ("one, two", DICT_STAT_SUCCESS) when queried +with foo, and will return (NOTFOUND, DICT_STAT_SUCCESS) +otherwise.

    + +

    First, we present the TEST_CASE structure with additional fields +for inputs and expected results.

    + +
    +
    + 29 #define MAX_PROBE       5
    + 30 
    + 31 struct probe {
    + 32     const char *query;
    + 33     const char *want_value;
    + 34     int     want_error;
    + 35 };
    + 36 
    + 37 typedef struct PTEST_CASE {
    + 38     const char *testname;
    + 39     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
    + 40     const char *type_name;
    + 41     const struct probe probes[MAX_PROBE];
    + 42 } PTEST_CASE;
    +
    +
    + +

    In the PTEST_CASE structure above:

    + +
      + +
    • The testname and action fields are +standard. We have seen these already in the simple example above. +

      + +

    • The type_name field will contain the name of the table, +for example unionmap:{static:one,inline:{foo=two}}.

      + +
    • The probes field contains a list of (query, expected +result value, expected error code) that will be used to query the unionmap +and to verify the result value and error code. +

      + +
    + +

    Next we show the test data. Every test calls the same +test_dict_union() function with a different unionmap +configuration and with a list of queries with expected results. The +implementation of that function follows after the test data.

    + +
    +
    + 78 static const PTEST_CASE ptestcases[] = {
    + 79     {
    + 80          /* testname */ "successful lookup: static map + inline map",
    + 81          /* action */ test_dict_union,
    + 82          /* type_name */ "unionmap:{static:one,inline:{foo=two}}",
    + 83          /* probes */ {
    + 84             {"foo", "one,two", DICT_STAT_SUCCESS},
    + 85             {"bar", "one", DICT_STAT_SUCCESS},
    + 86         },
    + 87     }, {
    + 88          /* testname */ "error propagation: static map + fail map",
    + 89          /* action */ test_dict_union,
    + 90          /* type_name */ "unionmap:{static:one,fail:fail}",
    + 91          /* probes */ {
    + 92             {"foo", 0, DICT_STAT_ERROR},
    + 93         },
    +...
    +102 };
    +103 
    +104 #include <ptest_main.h>
    +
    +
    + +

    Finally, here is the test_dict_union() function that +tests the unionmap implementation with a given configuration +and test queries.

    + +
    +
    + 44 #define STR_OR_NULL(s)  ((s) ? (s) : "null")
    + 45 
    + 46 static void test_dict_union(PTEST_CTX *t, const struct PTEST_CASE *tp)
    + 47 {
    + 48     DICT   *dict;
    + 49     const struct probe *pp;
    + 50     const char *got_value;
    + 51     int     got_error;
    + 52 
    + 53     if ((dict = dict_open(tp->type_name, O_RDONLY, 0)) == 0)
    + 54         ptest_fatal(t, "dict_open(\"%s\", O_RDONLY, 0) failed: %m",
    + 55                     tp->type_name);
    + 56     for (pp = tp->probes; pp < tp->probes + MAX_PROBE && pp->query != 0; pp++) {
    + 57         got_value = dict_get(dict, pp->query);
    + 58         got_error = dict->error;
    + 59         if (got_value == 0 && pp->want_value == 0)
    + 60             continue;
    + 61         if (got_value == 0 || pp->want_value == 0) {
    + 62             ptest_error(t, "dict_get(dict, \"%s\"): got '%s', want '%s'",
    + 63                         pp->query, STR_OR_NULL(got_value),
    + 64                         STR_OR_NULL(pp->want_value));
    + 65             break;
    + 66         }
    + 67         if (strcmp(got_value, pp->want_value) != 0) {
    + 68             ptest_error(t, "dict_get(dict, \"%s\"): got '%s', want '%s'",
    + 69                         pp->query, got_value, pp->want_value);
    + 70         }
    + 71         if (got_error != pp->want_error)
    + 72             ptest_error(t, "dict_get(dict,\"%s\") error: got %d, want %d",
    + 73                         pp->query, got_error, pp->want_error);
    + 74     }
    + 75     dict_free(dict);
    + 76 }
    +
    +
    + +

    A test run looks like this:

    + +
    +
    +$ make test_dict_union
    +...compiler output...
    +LD_LIBRARY_PATH=/path/to/postfix-source/lib ./dict_union_test
    +RUN  successful lookup: static map + inline map
    +PASS successful lookup: static map + inline map
    +RUN  error propagation: static map + fail map
    +PASS error propagation: static map + fail map
    +...
    +dict_union_test: PASS: 3, SKIP: 0, FAIL: 0
    +
    +
    + +

    Testing functions with subtests

    + +

    Sometimes it is not convenient to store test data in a PTEST_CASE +structure. This can happen when converting an existing test into +Ptest, or when the module under test contains multiple functions +that need different kinds of test data. The solution is to create +a _test.c file with the structure shown below. The example +is based on code in map_search_test.c that was converted +from an existing test into Ptest.

    + +
      + +
    • One PTEST_CASE structure definition without test data.

      + +
      + 50 typedef struct PTEST_CASE {
      + 51     const char *testname;
      + 52     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
      + 53 } PTEST_CASE;
      +
      + +
    • One test function for each module function that needs to +be tested, and one table with test cases for that module function. +In this case there is only one module function (map_search()) +that needs to be tested, so there is only one test function +(test_map_search()).

      + +
      + 67 #define MAX_WANT_LOG    5
      + 68 
      + 69 static void test_map_search(PTEST_CTX *t, const struct PTEST_CASE *unused)
      + 70 {
      + 71     /* Test cases with inputs and expected outputs. */
      + 72     struct test {
      + 73         const char *map_spec;
      + 74         int     want_return;            /* 0=fail, 1=success */
      + 75         const char *want_log[MAX_WANT_LOG];
      + 76         const char *want_map_type_name; /* 0 or match */
      + 77         const char *exp_search_order;   /* 0 or match */
      + 78     };
      + 79     static struct test test_cases[] = {
      + 80         { /* 0 */ "type", 0, {
      + 81                 "malformed map specification: 'type'",
      + 82                 "expected maptype:mapname instead of 'type'",
      + 83         }, 0},
      +...        // ...other test cases...
      +111     };
      +
      + +
    • In a test function, iterate over its table with test cases, +using PTEST_RUN() to run each test case in its own subtest. +

      + +
      +129     for (tp = test_cases; tp->map_spec; tp++) {
      +130         vstring_sprintf(test_label, "test %d", (int) (tp - test_cases));
      +131         PTEST_RUN(t, STR(test_label), {
      +132             for (cpp = tp->want_log; cpp < tp->want_log + MAX_WANT_LOG && *cpp; cpp++)
      +133                 expect_ptest_log_event(t, *cpp);
      +134             map_search_from_create = map_search_create(tp->map_spec);
      +...            // ...verify that the result is as expected...
      +...            // ...use ptest_return() or ptest_fatal() to exit from a test...
      +173         });
      +174     }
      +...
      +178 }
      +
      + +
    • Create a ptestcases[] table to call each test +function once, and include the Ptest main program.

      + +
      +183 static const PTEST_CASE ptestcases[] = {
      +184     "test_map_search", test_map_search,
      +185 };
      +186 
      +187 #include <ptest_main.h>
      +
      + +
    + +

    See the file map_search_test.c for a complete example. +

    + +

    This is what a test run looks like:

    + +
    +
    +$ make test_map_search
    +...compiler output...
    +LD_LIBRARY_PATH=/path/to/postfix-source/lib  ./map_search_test
    +RUN  test_map_search
    +RUN  test_map_search/test 0
    +PASS test_map_search/test 0
    +....
    +PASS test_map_search
    +map_search_test: PASS: 13, SKIP: 0, FAIL: 0
    +
    +
    + +

    This shows that the subtest name is appended to the parent test +name, formatted as parent-name/child-name.

    + +

    Suggestions for writing tests

    + +

    Ptest is loosely inspired on Go test, especially its top-level +test functions and its methods T.run(), T.error() +and T.fatal().

    + +

    Suggestions for test style may look familiar to Go programmers: +

    + +
      + +
    • Use variables named got_xxx and want_xxx, +and when a test result is unexpected, log the discrepancy as "got +<what you got>, want <what you want>".

      + +
    • Report discrepancies with ptest_error() if possible; +use ptest_fatal() only when continuing the test would +produce nonsensical results.

      + +
    • Where it makes sense use a table with testcases and use +PTEST_RUN() to run each testcase in its own subtest.

      + +
    + +

    Other suggestions:

    + +
      + +
    • Consider running tests under a memory checker such as +Valgrind. Use ptest_defer() to avoid memory leaks when a +test may terminate early.

      + +
    • Always test non-error and error cases, to cover all code +paths in the function under test.

      + +
    + +

    Ptest API reference

    + + + +

    Managing test errors

    + +

    As one might expect, Ptest has support to flag unexpected test +results as errors.

    + +
    + +
    void ptest_error(PTEST_CTX *t, +const char *format, ...)
    + +
    Called from inside a test to report an unexpected test result, +and to flag the test as failed without terminating the test. This +call can be ignored with expect_ptest_error().

    + +
    void ptest_fatal(PTEST_CTX *t, +const char *format, ...)
    + +
    Called from inside a test to report an unexpected test result, +to flag the test as failed, and to terminate the test. This call +cannot be ignored with expect_ptest_error().
    + +
    + +

    For convenience, Ptest has can also report non-error information. +

    + +
    + +
    void ptest_info(PTEST_CTX *t, +const char *format, ...)
    + +
    Called from inside a test to report a non-error condition +without terminating the test. This call cannot be ignored with +expect_ptest_error().
    + +
    + +

    Finally, Ptest has support to test ptest_error() itself, +to verify that an intentional error is reported as expected.

    + +
    + +
    void +expect_ptest_error(PTEST_CTX *t, const char *text) +
    + +
    Called from inside a test to expect exactly one ptest_error() +call with the specified text, and to ignore that ptest_error() +call (i.e. don't flag the test as failed). To ignore multiple calls, +call expect_ptest_error() multiple times. A test is flagged +as failed when an expected error is not reported (and of course +when an error is reported that is not expected with +expect_ptest_error()).
    + +
    + +

    Managing log events

    + +

    Ptest integrates with Postfix msg(3) logging.

    + +
      + +
    • Ptest changes the control flow of msg_fatal() and +msg_panic(). When these functions are called during a test, +Ptest flags a test as failed and terminates the test instead of the +process.

      + +
    • Ptest silences the output from msg_info() and +other msg(3) calls, and installs a log event listener tp +monitor Postfix logging.

      + +
    + +

    Ptest provides the following API to manage log events:

    + +
    + +
    void +expect_ptest_log_event(PTEST_CTX *t, const char *text) +
    + +
    Called from inside a test to expect exactly one msg(3) +call with the specified text. To expect multiple events, call +expect_ptest_log_event() multiple times. A test is flagged +as failed when expected text is not logged, or when text is logged +that is not expected with expect_ptest_log_event().
    + +
    + +

    Managing test execution

    + +

    Ptest has a number of primitives that control test execution. +

    + +
    + +
    void PTEST_RUN(PTEST_CTX *t, const +char *test_name, { code in braces })
    + +
    Called from inside a test to run the { code in braces +} in it own subtest environment. In the test progress report, +the subtest name is appended to the parent test name, formatted as +parent-name/child-name.

    NOTE: because PTEST_RUN() + is a macro, the { code in braces } must not contain +a return statement; use ptest_return() instead. +It is OK for { code in braces } to call a function that +uses return.

    + +
    NORETURN ptest_skip(PTEST_CTX +*t)
    + +
    Called from inside a test to flag a test as skipped, and to +terminate the test without terminating the process. Use this to +disable tests that are not applicable for a specific system type +or build configuration.

    + +
    NORETURN ptest_return(PTEST_CTX +*t)
    + +
    Called from inside a test to terminate the test without +terminating the process.

    + +
    void ptest_defer(PTEST_CTX *t, +void (*defer_fn)(void *), void *defer_ctx)
    + +
    Called once from inside a test, to call defer_fn(defer_ctx) +after the test completes. This is typically used to eliminate a +resource leak in tests that terminate the test early.

    +NOTE: The deferred function is designed to run outside a test, and +therefore it must not call Ptest functions.
    + +
    + + + + + diff --git a/postfix/proto/SASL_README.html b/postfix/proto/SASL_README.html index c3aaad7bd..3e2025a7a 100644 --- a/postfix/proto/SASL_README.html +++ b/postfix/proto/SASL_README.html @@ -280,6 +280,14 @@ configuration file in /etc/postfix/sasl/, cyrus_sasl_config_path and/or the distribution-specific documentation to determine the expected location.

  • +
  • Some Debian-based Postfix distributions patch Postfix to +hardcode a non-default search path, making it impossible to set an +alternate search path via the "cyrus_sasl_config_path" parameter. This +is likely to be the case when the distribution documents a +Postfix-specific path (e.g. /etc/postfix/sasl/) that is +different from the default value of "cyrus_sasl_config_path" (which +then is likely to be empty).

  • +
    diff --git a/postfix/proto/SMTPD_POLICY_README.html b/postfix/proto/SMTPD_POLICY_README.html index 189fb08dd..dd0a5494a 100644 --- a/postfix/proto/SMTPD_POLICY_README.html +++ b/postfix/proto/SMTPD_POLICY_README.html @@ -118,6 +118,7 @@ server_address=10.3.2.1 server_port=54321 Postfix version 3.8 and later: compatibility_level=major.minor.patch +mail_version=3.8.0 [empty line]
    @@ -220,6 +221,12 @@ compatibility_level=major.minor.patch major.minor.patch where minor and patch may be absent.

    +
  • The "mail_version" attribute corresponds to the + mail_version parameter value. It has the form + major.minor.patch for stable releases, and + major.minor-yyyymmdd for unstable releases. +

    +

    The following is specific to SMTPD delegated policy requests: diff --git a/postfix/proto/aliases b/postfix/proto/aliases index ed01ec0c9..d2d3f19b5 100644 --- a/postfix/proto/aliases +++ b/postfix/proto/aliases @@ -65,14 +65,17 @@ # Mail is forwarded to \fIaddress\fR, which is compatible # with the RFC 822 standard. # .IP \fI/file/name\fR -# Mail is appended to \fI/file/name\fR. See \fBlocal\fR(8) -# for details of delivery to file. +# Mail is appended to \fI/file/name\fR. For details on how a +# file is written see the sections "EXTERNAL FILE DELIVERY" +# and "DELIVERY RIGHTS" in the \fBlocal\fR(8) documentation. # Delivery is not limited to regular files. For example, to dispose # of unwanted mail, deflect it to \fB/dev/null\fR. # .IP "|\fIcommand\fR" -# Mail is piped into \fIcommand\fR. Commands that contain special -# characters, such as whitespace, should be enclosed between double -# quotes. See \fBlocal\fR(8) for details of delivery to command. +# Mail is piped into \fIcommand\fR. Commands that contain +# special characters, such as whitespace, should be enclosed +# between double quotes. For details on how a command is +# executed see "EXTERNAL COMMAND DELIVERY" and "DELIVERY +# RIGHTS" in the \fBlocal\fR(8) documentation. # .sp # When the command fails, a limited amount of command output is # mailed back to the sender. The file \fB/usr/include/sysexits.h\fR diff --git a/postfix/proto/postconf.proto b/postfix/proto/postconf.proto index 76919f0ca..0f335eea9 100644 --- a/postfix/proto/postconf.proto +++ b/postfix/proto/postconf.proto @@ -14354,13 +14354,16 @@ The default time unit is s (seconds).

    %PARAM postscreen_dnsbl_sites -

    Optional list of DNS allow/denylist domains, filters and weight +

    Optional list of patterns with DNS allow/denylist domains, filters +and weight factors. When the list is non-empty, the dnsblog(8) daemon will -query these domains with the IP addresses of remote SMTP clients, +query these domains with the reversed IP addresses of remote SMTP +clients, and postscreen(8) will update an SMTP client's DNSBL score with -each non-error reply.

    +each non-error reply as described below.

    -

    Caution: when postscreen rejects mail, it replies with the DNSBL +

    Caution: when postscreen rejects mail, its SMTP response contains +the DNSBL domain name. Use the postscreen_dnsbl_reply_map feature to hide "password" information in DNSBL domain names.

    @@ -14368,26 +14371,25 @@ domain name. Use the postscreen_dnsbl_reply_map feature to hide specified with postscreen_dnsbl_threshold, postscreen(8) can drop the connection with the remote SMTP client.

    -

    Specify a list of domain=filter*weight entries, separated by +

    Specify a list of domain=filter*weight patterns, separated by comma or whitespace.

      -
    • When no "=filter" is specified, postscreen(8) will use any -non-error DNSBL reply. Otherwise, postscreen(8) uses only DNSBL -replies that match the filter. The filter has the form d.d.d.d, +

    • When a pattern specifies no "=filter", postscreen(8) will +use any non-error DNSBL query result. Otherwise, postscreen(8) +will use only DNSBL +query results that match the filter. The filter has the form d.d.d.d, where each d is a number, or a pattern inside [] that contains one or more ";"-separated numbers or number..number ranges.

      -
    • When no "*weight" is specified, postscreen(8) increments -the remote SMTP client's DNSBL score by 1. Otherwise, the weight must be -an integral number, and postscreen(8) adds the specified weight to -the remote SMTP client's DNSBL score. Specify a negative number for -allowlisting.

      +
    • When a pattern specifies no "*weight", the weight of the +pattern is 1. Otherwise, the weight must be an integral number. +Specify a negative number for allowlisting.

      -
    • When one postscreen_dnsbl_sites entry produces multiple -DNSBL responses, postscreen(8) applies the weight at most once. -

      +
    • When a pattern matches one or more DNSBL query results, +postscreen(8) adds that pattern's weight once to the remote SMTP +client's DNSBL score.

    diff --git a/postfix/proto/stop.double-proto-html b/postfix/proto/stop.double-proto-html index a7e78243d..854800873 100644 --- a/postfix/proto/stop.double-proto-html +++ b/postfix/proto/stop.double-proto-html @@ -245,3 +245,7 @@ dt dt b name value b Postfix ge 3 0 dt dt dt dd 4 Also log the hexadecimal and ASCII dump of complete parametername stress something something Other p Note on OpenBSD systems specify dev dev arandom when dev dev urandom + 90 type_name unionmap static one fail fail + a structure as shown shown below The example is based on code in +184 test_map_search test_map_search + void ptest_defer PTEST_CTX t void defer_fn void void diff --git a/postfix/proto/stop.spell-cc b/postfix/proto/stop.spell-cc index 550d998d0..1289948b5 100644 --- a/postfix/proto/stop.spell-cc +++ b/postfix/proto/stop.spell-cc @@ -1838,3 +1838,10 @@ ptestcase ptestcases subtests case's +HAPROXY +SRVR +Deserialize +SNDBUF +deserialize +deserialized +NORAMDOMIZE diff --git a/postfix/proto/stop.spell-proto-html b/postfix/proto/stop.spell-proto-html index 616fb118e..9aeac1844 100644 --- a/postfix/proto/stop.spell-proto-html +++ b/postfix/proto/stop.spell-proto-html @@ -349,3 +349,42 @@ J ng rsyslogd ptest +DICT +Gmock +LD +NORETURN +NOTFOUND +PTEST +Pmock +Ptest +RDONLY +STAT +STR +Valgrind +addrinfo +api +const +cpp +eq +exp +fn +myfree +mymalloc +pp +ptestcases +struct +subtests +testcase +testcases +testname +tp +vstring +subtest +templating +typedef +Gtest +mymemdup +myrealloc +mystrdup +hardcode +pattern's diff --git a/postfix/src/global/Makefile.in b/postfix/src/global/Makefile.in index a8c7ccd4c..02ed18856 100644 --- a/postfix/src/global/Makefile.in +++ b/postfix/src/global/Makefile.in @@ -83,7 +83,7 @@ OBJS = abounce.o anvil_clnt.o been_here.o bounce.o bounce_log.o \ MAP_OBJ = dict_ldap.o dict_mysql.o dict_pgsql.o dict_sqlite.o TEST_OBJ = normalize_mailhost_addr_test.o smtp_reply_footer_test.o \ login_sender_match_test.o map_search_test.o delivered_hdr_test.o \ - config_known_tcp_ports_test.o hfrom_format_test.o + config_known_tcp_ports_test.o hfrom_format_test.o haproxy_srvr_test.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 \ @@ -133,12 +133,13 @@ TESTPROG= domain_list dot_lockfile mail_addr_crunch mail_addr_find \ data_redirect addr_match_list safe_ultostr verify_sender_addr \ mail_version mail_dict server_acl uxtext mail_parm_split \ fold_addr smtp_reply_footer_test mail_addr_map \ - normalize_mailhost_addr_test haproxy_srvr map_search_test \ + normalize_mailhost_addr_test haproxy_srvr_test map_search_test \ delivered_hdr_test login_sender_match_test \ compat_level config_known_tcp_ports_test hfrom_format_test LIBS = ../../lib/lib$(LIB_PREFIX)util$(LIB_SUFFIX) -TEST_LIB= ../../lib/libtesting.a ../../lib/libptest.a +PTEST_LIB= ../../lib/libptest.a +PMOCK_LIB= ../../lib/libtesting.a LIB_DIR = ../../lib INC_DIR = ../../include PLUGIN_MAP_SO = $(LIB_PREFIX)ldap$(LIB_SUFFIX) $(LIB_PREFIX)mysql$(LIB_SUFFIX) \ @@ -390,7 +391,7 @@ tests: tok822_test mime_tests strip_addr_test tok822_limit_test \ safe_ultostr_test mail_parm_split_test fold_addr_test \ test_smtp_reply_footer off_cvt_test mail_addr_crunch_test \ mail_addr_find_test mail_addr_map_test quote_822_local_test \ - test_normalize_mailhost_addr haproxy_srvr_test test_map_search \ + test_normalize_mailhost_addr test_haproxy_srvr test_map_search \ delivered_hdr_test test_login_sender_match compat_level_test \ test_config_known_tcp_ports test_hfrom_format @@ -674,8 +675,8 @@ fold_addr_test: fold_addr fold_addr_test.in fold_addr_test.ref diff fold_addr_test.ref fold_addr_test.tmp rm -f fold_addr_test.tmp -smtp_reply_footer_test: smtp_reply_footer_test.o $(TEST_LIB) $(LIB) $(LIBS) - $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) +smtp_reply_footer_test: smtp_reply_footer_test.o $(PTEST_LIB) $(LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) test_smtp_reply_footer: smtp_reply_footer_test $(SHLIB_ENV) $(VALGRIND) ./smtp_reply_footer_test @@ -707,32 +708,34 @@ quote_822_local_test: update quote_822_local quote_822_local.in quote_822_local. rm -f quote_822_local.tmp normalize_mailhost_addr_test: normalize_mailhost_addr_test.o \ - $(TEST_LIB) $(LIB) $(LIBS) - $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) + $(PTEST_LIB) $(LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) test_normalize_mailhost_addr: update normalize_mailhost_addr_test $(SHLIB_ENV) $(VALGRIND) ./normalize_mailhost_addr_test -haproxy_srvr_test: update haproxy_srvr - -$(SHLIB_ENV) $(VALGRIND) ./haproxy_srvr >haproxy_srvr.tmp 2>&1 - diff /dev/null haproxy_srvr.tmp - rm -f haproxy_srvr.tmp +haproxy_srvr_test: haproxy_srvr_test.o \ + $(PTEST_LIB) $(LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) -map_search_test: map_search_test.o $(TEST_LIB) $(LIB) $(LIBS) - $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) +test_haproxy_srvr: update haproxy_srvr_test + $(SHLIB_ENV) $(VALGRIND) ./haproxy_srvr_test + +map_search_test: map_search_test.o $(PTEST_LIB) $(LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) test_map_search: update map_search_test $(SHLIB_ENV) $(VALGRIND) ./map_search_test -delivered_hdr_test: delivered_hdr_test.o $(TEST_LIB) $(LIB) $(LIBS) - $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) +delivered_hdr_test: delivered_hdr_test.o $(PTEST_LIB) $(LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) test_delivered_hdr: update delivered_hdr_test $(SHLIB_ENV) $(VALGRIND) ./delivered_hdr_test login_sender_match_test: login_sender_match_test.o \ - $(TEST_LIB) $(LIB) $(LIBS) - $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) + $(PTEST_LIB) $(LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) test_login_sender_match: update login_sender_match_test $(SHLIB_ENV) $(VALGRIND) ./login_sender_match_test @@ -751,14 +754,14 @@ compat_level_convert_test: update compat_level compat_level_convert.in \ diff compat_level_convert.ref compat_level_convert.tmp rm -f compat_level_convert.tmp -config_known_tcp_ports_test: config_known_tcp_ports_test.o $(TEST_LIB) $(LIB) $(LIBS) - $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) +config_known_tcp_ports_test: config_known_tcp_ports_test.o $(PTEST_LIB) $(LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) test_config_known_tcp_ports: update config_known_tcp_ports_test $(SHLIB_ENV) $(VALGRIND) ./config_known_tcp_ports_test -hfrom_format_test: hfrom_format_test.o $(TEST_LIB) $(LIB) $(LIBS) - $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) +hfrom_format_test: hfrom_format_test.o $(PTEST_LIB) $(LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS) test_hfrom_format: update hfrom_format_test $(SHLIB_ENV) $(VALGRIND) ./hfrom_format_test @@ -1197,26 +1200,8 @@ delivered_hdr_test.o: fold_addr.h delivered_hdr_test.o: mail_params.h delivered_hdr_test.o: rec_type.h delivered_hdr_test.o: record.h -dict_ldap.o: ../../include/argv.h -dict_ldap.o: ../../include/binhash.h -dict_ldap.o: ../../include/check_arg.h -dict_ldap.o: ../../include/dict.h -dict_ldap.o: ../../include/match_list.h -dict_ldap.o: ../../include/msg.h -dict_ldap.o: ../../include/myflock.h -dict_ldap.o: ../../include/mymalloc.h -dict_ldap.o: ../../include/name_code.h -dict_ldap.o: ../../include/stringops.h dict_ldap.o: ../../include/sys_defs.h -dict_ldap.o: ../../include/vbuf.h -dict_ldap.o: ../../include/vstream.h -dict_ldap.o: ../../include/vstring.h -dict_ldap.o: cfg_parser.h -dict_ldap.o: db_common.h dict_ldap.o: dict_ldap.c -dict_ldap.o: dict_ldap.h -dict_ldap.o: mail_conf.h -dict_ldap.o: string_list.h dict_memcache.o: ../../include/argv.h dict_memcache.o: ../../include/auto_clnt.h dict_memcache.o: ../../include/check_arg.h @@ -1236,47 +1221,10 @@ 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_mysql.o: ../../include/argv.h -dict_mysql.o: ../../include/check_arg.h -dict_mysql.o: ../../include/dict.h -dict_mysql.o: ../../include/events.h -dict_mysql.o: ../../include/find_inet.h -dict_mysql.o: ../../include/match_list.h -dict_mysql.o: ../../include/msg.h -dict_mysql.o: ../../include/myflock.h -dict_mysql.o: ../../include/mymalloc.h -dict_mysql.o: ../../include/myrand.h -dict_mysql.o: ../../include/split_at.h -dict_mysql.o: ../../include/stringops.h dict_mysql.o: ../../include/sys_defs.h -dict_mysql.o: ../../include/vbuf.h -dict_mysql.o: ../../include/vstream.h -dict_mysql.o: ../../include/vstring.h -dict_mysql.o: cfg_parser.h -dict_mysql.o: db_common.h dict_mysql.o: dict_mysql.c -dict_mysql.o: dict_mysql.h -dict_mysql.o: string_list.h -dict_pgsql.o: ../../include/argv.h -dict_pgsql.o: ../../include/check_arg.h -dict_pgsql.o: ../../include/dict.h -dict_pgsql.o: ../../include/events.h -dict_pgsql.o: ../../include/match_list.h -dict_pgsql.o: ../../include/msg.h -dict_pgsql.o: ../../include/myflock.h -dict_pgsql.o: ../../include/mymalloc.h -dict_pgsql.o: ../../include/myrand.h -dict_pgsql.o: ../../include/split_at.h -dict_pgsql.o: ../../include/stringops.h dict_pgsql.o: ../../include/sys_defs.h -dict_pgsql.o: ../../include/vbuf.h -dict_pgsql.o: ../../include/vstream.h -dict_pgsql.o: ../../include/vstring.h -dict_pgsql.o: cfg_parser.h -dict_pgsql.o: db_common.h dict_pgsql.o: dict_pgsql.c -dict_pgsql.o: dict_pgsql.h -dict_pgsql.o: string_list.h dict_proxy.o: ../../include/argv.h dict_proxy.o: ../../include/attr.h dict_proxy.o: ../../include/check_arg.h @@ -1298,23 +1246,8 @@ dict_proxy.o: dict_proxy.c dict_proxy.o: dict_proxy.h dict_proxy.o: mail_params.h dict_proxy.o: mail_proto.h -dict_sqlite.o: ../../include/argv.h -dict_sqlite.o: ../../include/check_arg.h -dict_sqlite.o: ../../include/dict.h -dict_sqlite.o: ../../include/match_list.h -dict_sqlite.o: ../../include/msg.h -dict_sqlite.o: ../../include/myflock.h -dict_sqlite.o: ../../include/mymalloc.h -dict_sqlite.o: ../../include/stringops.h dict_sqlite.o: ../../include/sys_defs.h -dict_sqlite.o: ../../include/vbuf.h -dict_sqlite.o: ../../include/vstream.h -dict_sqlite.o: ../../include/vstring.h -dict_sqlite.o: cfg_parser.h -dict_sqlite.o: db_common.h dict_sqlite.o: dict_sqlite.c -dict_sqlite.o: dict_sqlite.h -dict_sqlite.o: string_list.h domain_list.o: ../../include/argv.h domain_list.o: ../../include/check_arg.h domain_list.o: ../../include/match_list.h @@ -1506,6 +1439,24 @@ haproxy_srvr.o: ../../include/vstring.h haproxy_srvr.o: ../../include/wrap_netdb.h haproxy_srvr.o: haproxy_srvr.c haproxy_srvr.o: haproxy_srvr.h +haproxy_srvr_test.o: ../../include/argv.h +haproxy_srvr_test.o: ../../include/check_arg.h +haproxy_srvr_test.o: ../../include/msg.h +haproxy_srvr_test.o: ../../include/msg_output.h +haproxy_srvr_test.o: ../../include/msg_vstream.h +haproxy_srvr_test.o: ../../include/myaddrinfo.h +haproxy_srvr_test.o: ../../include/pmock_expect.h +haproxy_srvr_test.o: ../../include/ptest.h +haproxy_srvr_test.o: ../../include/ptest_main.h +haproxy_srvr_test.o: ../../include/sock_addr.h +haproxy_srvr_test.o: ../../include/stringops.h +haproxy_srvr_test.o: ../../include/sys_defs.h +haproxy_srvr_test.o: ../../include/vbuf.h +haproxy_srvr_test.o: ../../include/vstream.h +haproxy_srvr_test.o: ../../include/vstring.h +haproxy_srvr_test.o: ../../include/wrap_netdb.h +haproxy_srvr_test.o: haproxy_srvr.h +haproxy_srvr_test.o: haproxy_srvr_test.c header_body_checks.o: ../../include/argv.h header_body_checks.o: ../../include/check_arg.h header_body_checks.o: ../../include/dict.h diff --git a/postfix/src/global/haproxy_srvr.c b/postfix/src/global/haproxy_srvr.c index 63147c1c5..e349128c3 100644 --- a/postfix/src/global/haproxy_srvr.c +++ b/postfix/src/global/haproxy_srvr.c @@ -85,6 +85,7 @@ /* Global library. */ +#define HAPROXY_SRVR_INTERNAL #include /* Application-specific. */ @@ -98,73 +99,6 @@ */ #define HAPROXY_HEADER_MAX_LEN 536 - /* - * Begin protocol v2 definitions from haproxy/include/types/connection.h. - */ -#define PP2_SIGNATURE "\r\n\r\n\0\r\nQUIT\n" -#define PP2_SIGNATURE_LEN 12 -#define PP2_HEADER_LEN 16 - -/* ver_cmd byte */ -#define PP2_CMD_LOCAL 0x00 -#define PP2_CMD_PROXY 0x01 -#define PP2_CMD_MASK 0x0F - -#define PP2_VERSION 0x20 -#define PP2_VERSION_MASK 0xF0 - -/* fam byte */ -#define PP2_TRANS_UNSPEC 0x00 -#define PP2_TRANS_STREAM 0x01 -#define PP2_TRANS_DGRAM 0x02 -#define PP2_TRANS_MASK 0x0F - -#define PP2_FAM_UNSPEC 0x00 -#define PP2_FAM_INET 0x10 -#define PP2_FAM_INET6 0x20 -#define PP2_FAM_UNIX 0x30 -#define PP2_FAM_MASK 0xF0 - -/* len field (2 bytes) */ -#define PP2_ADDR_LEN_UNSPEC (0) -#define PP2_ADDR_LEN_INET (4 + 4 + 2 + 2) -#define PP2_ADDR_LEN_INET6 (16 + 16 + 2 + 2) -#define PP2_ADDR_LEN_UNIX (108 + 108) - -#define PP2_HDR_LEN_UNSPEC (PP2_HEADER_LEN + PP2_ADDR_LEN_UNSPEC) -#define PP2_HDR_LEN_INET (PP2_HEADER_LEN + PP2_ADDR_LEN_INET) -#define PP2_HDR_LEN_INET6 (PP2_HEADER_LEN + PP2_ADDR_LEN_INET6) -#define PP2_HDR_LEN_UNIX (PP2_HEADER_LEN + PP2_ADDR_LEN_UNIX) - -struct proxy_hdr_v2 { - uint8_t sig[PP2_SIGNATURE_LEN]; /* PP2_SIGNATURE */ - uint8_t ver_cmd; /* protocol version | command */ - uint8_t fam; /* protocol family and transport */ - uint16_t len; /* length of remainder */ - union { - struct { /* for TCP/UDP over IPv4, len = 12 */ - uint32_t src_addr; - uint32_t dst_addr; - uint16_t src_port; - uint16_t dst_port; - } ip4; - struct { /* for TCP/UDP over IPv6, len = 36 */ - uint8_t src_addr[16]; - uint8_t dst_addr[16]; - uint16_t src_port; - uint16_t dst_port; - } ip6; - struct { /* for AF_UNIX sockets, len = 216 */ - uint8_t src_addr[108]; - uint8_t dst_addr[108]; - } unx; - } addr; -}; - - /* - * End protocol v2 definitions from haproxy/include/types/connection.h. - */ - static const INET_PROTO_INFO *proto_info; #define STR_OR_NULL(str) ((str) ? (str) : "(null)") @@ -535,357 +469,3 @@ int haproxy_srvr_receive(int fd, int *non_proxy, } return (0); } - - /* - * Test program. - */ -#ifdef TEST - - /* - * Test cases with inputs and expected outputs. A request may contain - * trailing garbage, and it may be too short. A v1 request may also contain - * malformed address or port information. - */ -typedef struct TEST_CASE { - const char *haproxy_request; /* v1 or v2 request including thrash */ - ssize_t haproxy_req_len; /* request length including thrash */ - ssize_t exp_req_len; /* parsed request length */ - int exp_non_proxy; /* request is not proxied */ - const char *exp_return; /* expected error string */ - const char *exp_client_addr; /* expected client address string */ - const char *exp_server_addr; /* expected client port string */ - const char *exp_client_port; /* expected client address string */ - const char *exp_server_port; /* expected server port string */ -} TEST_CASE; -static TEST_CASE v1_test_cases[] = { - /* IPv6. */ - {"PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1 123 321\n", 0, 0, 0, 0, "fc::1:2:3:4", "fc::4:3:2:1", "123", "321"}, - {"PROXY TCP6 FC:00:00:00:1:2:3:4 FC:00:00:00:4:3:2:1 123 321\n", 0, 0, 0, 0, "fc::1:2:3:4", "fc::4:3:2:1", "123", "321"}, - {"PROXY TCP6 1.2.3.4 4.3.2.1 123 321\n", 0, 0, 0, "bad or missing client address"}, - {"PROXY TCP6 fc:00:00:00:1:2:3:4 4.3.2.1 123 321\n", 0, 0, 0, "bad or missing server address"}, - /* IPv4 in IPv6. */ - {"PROXY TCP6 ::ffff:1.2.3.4 ::ffff:4.3.2.1 123 321\n", 0, 0, 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"}, - {"PROXY TCP6 ::FFFF:1.2.3.4 ::FFFF:4.3.2.1 123 321\n", 0, 0, 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"}, - {"PROXY TCP4 ::ffff:1.2.3.4 ::ffff:4.3.2.1 123 321\n", 0, 0, 0, "bad or missing client address"}, - {"PROXY TCP4 1.2.3.4 ::ffff:4.3.2.1 123 321\n", 0, 0, 0, "bad or missing server address"}, - /* IPv4. */ - {"PROXY TCP4 1.2.3.4 4.3.2.1 123 321\n", 0, 0, 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"}, - {"PROXY TCP4 01.02.03.04 04.03.02.01 123 321\n", 0, 0, 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"}, - {"PROXY TCP4 1.2.3.4 4.3.2.1 123456 321\n", 0, 0, 0, "bad or missing client port"}, - {"PROXY TCP4 1.2.3.4 4.3.2.1 123 654321\n", 0, 0, 0, "bad or missing server port"}, - {"PROXY TCP4 1.2.3.4 4.3.2.1 0123 321\n", 0, 0, 0, "bad or missing client port"}, - {"PROXY TCP4 1.2.3.4 4.3.2.1 123 0321\n", 0, 0, 0, "bad or missing server port"}, - /* Missing fields. */ - {"PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1 123\n", 0, 0, 0, "bad or missing server port"}, - {"PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1\n", 0, 0, 0, "bad or missing client port"}, - {"PROXY TCP6 fc:00:00:00:1:2:3:4\n", 0, 0, 0, "bad or missing server address"}, - {"PROXY TCP6\n", 0, 0, 0, "bad or missing client address"}, - {"PROXY TCP4 1.2.3.4 4.3.2.1 123\n", 0, 0, 0, "bad or missing server port"}, - {"PROXY TCP4 1.2.3.4 4.3.2.1\n", 0, 0, 0, "bad or missing client port"}, - {"PROXY TCP4 1.2.3.4\n", 0, 0, 0, "bad or missing server address"}, - {"PROXY TCP4\n", 0, 0, 0, "bad or missing client address"}, - /* Other. */ - {"PROXY BLAH\n", 0, 0, 0, "bad or missing protocol type"}, - {"PROXY\n", 0, 0, 0, "short protocol header"}, - {"BLAH\n", 0, 0, 0, "short protocol header"}, - {"\n", 0, 0, 0, "short protocol header"}, - {"", 0, 0, 0, "short protocol header"}, - 0, -}; - -static struct proxy_hdr_v2 v2_local_request = { - PP2_SIGNATURE, PP2_VERSION | PP2_CMD_LOCAL, -}; -static TEST_CASE v2_non_proxy_test = { - (char *) &v2_local_request, PP2_HEADER_LEN, PP2_HEADER_LEN, 1, -}; - -#define STR(x) vstring_str(x) -#define LEN(x) VSTRING_LEN(x) - -/* evaluate_test_case - evaluate one test case */ - -static int evaluate_test_case(const char *test_label, - const TEST_CASE *test_case) -{ - /* Actual results. */ - const char *act_return; - ssize_t act_req_len; - int act_non_proxy; - MAI_HOSTADDR_STR act_smtp_client_addr; - MAI_HOSTADDR_STR act_smtp_server_addr; - MAI_SERVPORT_STR act_smtp_client_port; - MAI_SERVPORT_STR act_smtp_server_port; - int test_failed; - - if (msg_verbose) - msg_info("test case=%s exp_client_addr=%s exp_server_addr=%s " - "exp_client_port=%s exp_server_port=%s", - test_label, STR_OR_NULL(test_case->exp_client_addr), - STR_OR_NULL(test_case->exp_server_addr), - STR_OR_NULL(test_case->exp_client_port), - STR_OR_NULL(test_case->exp_server_port)); - - /* - * Start the test. - */ - test_failed = 0; - act_req_len = test_case->haproxy_req_len; - act_return = - haproxy_srvr_parse(test_case->haproxy_request, &act_req_len, - &act_non_proxy, - &act_smtp_client_addr, &act_smtp_client_port, - &act_smtp_server_addr, &act_smtp_server_port); - if (act_return != test_case->exp_return) { - msg_warn("test case %s return expected=%s actual=%s", - test_label, STR_OR_NULL(test_case->exp_return), - STR_OR_NULL(act_return)); - test_failed = 1; - return (test_failed); - } - if (act_req_len != test_case->exp_req_len) { - msg_warn("test case %s str_len expected=%ld actual=%ld", - test_label, - (long) test_case->exp_req_len, (long) act_req_len); - test_failed = 1; - return (test_failed); - } - if (act_non_proxy != test_case->exp_non_proxy) { - msg_warn("test case %s non_proxy expected=%d actual=%d", - test_label, - test_case->exp_non_proxy, act_non_proxy); - test_failed = 1; - return (test_failed); - } - if (test_case->exp_non_proxy || test_case->exp_return != 0) - /* No expected address/port results. */ - return (test_failed); - - /* - * Compare address/port results against expected results. - */ - if (strcmp(test_case->exp_client_addr, act_smtp_client_addr.buf)) { - msg_warn("test case %s client_addr expected=%s actual=%s", - test_label, - test_case->exp_client_addr, act_smtp_client_addr.buf); - test_failed = 1; - } - if (strcmp(test_case->exp_server_addr, act_smtp_server_addr.buf)) { - msg_warn("test case %s server_addr expected=%s actual=%s", - test_label, - test_case->exp_server_addr, act_smtp_server_addr.buf); - test_failed = 1; - } - if (strcmp(test_case->exp_client_port, act_smtp_client_port.buf)) { - msg_warn("test case %s client_port expected=%s actual=%s", - test_label, - test_case->exp_client_port, act_smtp_client_port.buf); - test_failed = 1; - } - if (strcmp(test_case->exp_server_port, act_smtp_server_port.buf)) { - msg_warn("test case %s server_port expected=%s actual=%s", - test_label, - test_case->exp_server_port, act_smtp_server_port.buf); - test_failed = 1; - } - return (test_failed); -} - -/* convert_v1_proxy_req_to_v2 - convert well-formed v1 proxy request to v2 */ - -static void convert_v1_proxy_req_to_v2(VSTRING *buf, const char *req, - ssize_t req_len) -{ - const char myname[] = "convert_v1_proxy_req_to_v2"; - const char *err; - int non_proxy; - MAI_HOSTADDR_STR smtp_client_addr; - MAI_SERVPORT_STR smtp_client_port; - MAI_HOSTADDR_STR smtp_server_addr; - MAI_SERVPORT_STR smtp_server_port; - struct proxy_hdr_v2 *hdr_v2; - struct addrinfo *src_res; - struct addrinfo *dst_res; - - /* - * Allocate buffer space for the largest possible protocol header, so we - * don't have to worry about hidden realloc() calls. - */ - VSTRING_RESET(buf); - VSTRING_SPACE(buf, sizeof(struct proxy_hdr_v2)); - hdr_v2 = (struct proxy_hdr_v2 *) STR(buf); - - /* - * Fill in the header, - */ - memcpy(hdr_v2->sig, PP2_SIGNATURE, PP2_SIGNATURE_LEN); - hdr_v2->ver_cmd = PP2_VERSION | PP2_CMD_PROXY; - if ((err = haproxy_srvr_parse(req, &req_len, &non_proxy, &smtp_client_addr, - &smtp_client_port, &smtp_server_addr, - &smtp_server_port)) != 0 || non_proxy) - msg_fatal("%s: malformed or non-proxy request: %s", - myname, req); - - if (hostaddr_to_sockaddr(smtp_client_addr.buf, smtp_client_port.buf, 0, - &src_res) != 0) - msg_fatal("%s: unable to convert source address %s port %s", - myname, smtp_client_addr.buf, smtp_client_port.buf); - if (hostaddr_to_sockaddr(smtp_server_addr.buf, smtp_server_port.buf, 0, - &dst_res) != 0) - msg_fatal("%s: unable to convert destination address %s port %s", - myname, smtp_server_addr.buf, smtp_server_port.buf); - if (src_res->ai_family != dst_res->ai_family) - msg_fatal("%s: mixed source/destination address families", myname); -#ifdef AF_INET6 - if (src_res->ai_family == PF_INET6) { - hdr_v2->fam = PP2_FAM_INET6 | PP2_TRANS_STREAM; - hdr_v2->len = htons(PP2_ADDR_LEN_INET6); - memcpy(hdr_v2->addr.ip6.src_addr, - &SOCK_ADDR_IN6_ADDR(src_res->ai_addr), - sizeof(hdr_v2->addr.ip6.src_addr)); - hdr_v2->addr.ip6.src_port = SOCK_ADDR_IN6_PORT(src_res->ai_addr); - memcpy(hdr_v2->addr.ip6.dst_addr, - &SOCK_ADDR_IN6_ADDR(dst_res->ai_addr), - sizeof(hdr_v2->addr.ip6.dst_addr)); - hdr_v2->addr.ip6.dst_port = SOCK_ADDR_IN6_PORT(dst_res->ai_addr); - } else -#endif - if (src_res->ai_family == PF_INET) { - hdr_v2->fam = PP2_FAM_INET | PP2_TRANS_STREAM; - hdr_v2->len = htons(PP2_ADDR_LEN_INET); - hdr_v2->addr.ip4.src_addr = SOCK_ADDR_IN_ADDR(src_res->ai_addr).s_addr; - hdr_v2->addr.ip4.src_port = SOCK_ADDR_IN_PORT(src_res->ai_addr); - hdr_v2->addr.ip4.dst_addr = SOCK_ADDR_IN_ADDR(dst_res->ai_addr).s_addr; - hdr_v2->addr.ip4.dst_port = SOCK_ADDR_IN_PORT(dst_res->ai_addr); - } else { - msg_panic("unknown address family 0x%x", src_res->ai_family); - } - vstring_set_payload_size(buf, PP2_SIGNATURE_LEN + ntohs(hdr_v2->len)); - freeaddrinfo(src_res); - freeaddrinfo(dst_res); -} - -int main(int argc, char **argv) -{ - VSTRING *test_label; - TEST_CASE *v1_test_case; - TEST_CASE v2_test_case; - TEST_CASE mutated_test_case; - VSTRING *v2_request_buf; - VSTRING *mutated_request_buf; - - /* Findings. */ - int tests_failed = 0; - int test_failed; - - test_label = vstring_alloc(100); - v2_request_buf = vstring_alloc(100); - mutated_request_buf = vstring_alloc(100); - - for (tests_failed = 0, v1_test_case = v1_test_cases; - v1_test_case->haproxy_request != 0; - tests_failed += test_failed, v1_test_case++) { - - /* - * Fill in missing string length info in v1 test data. - */ - if (v1_test_case->haproxy_req_len == 0) - v1_test_case->haproxy_req_len = - strlen(v1_test_case->haproxy_request); - if (v1_test_case->exp_req_len == 0) - v1_test_case->exp_req_len = v1_test_case->haproxy_req_len; - - /* - * Evaluate each v1 test case. - */ - vstring_sprintf(test_label, "%d", (int) (v1_test_case - v1_test_cases)); - test_failed = evaluate_test_case(STR(test_label), v1_test_case); - - /* - * If the v1 test input is malformed, skip the mutation tests. - */ - if (v1_test_case->exp_return != 0) - continue; - - /* - * Mutation test: a well-formed v1 test case should still pass after - * appending a byte, and should return the actual parsed header - * length. The test uses the implicit VSTRING null safety byte. - */ - vstring_sprintf(test_label, "%d (one byte appended)", - (int) (v1_test_case - v1_test_cases)); - mutated_test_case = *v1_test_case; - mutated_test_case.haproxy_req_len += 1; - /* reuse v1_test_case->exp_req_len */ - test_failed += evaluate_test_case(STR(test_label), &mutated_test_case); - - /* - * Mutation test: a well-formed v1 test case should fail after - * stripping the terminator. - */ - vstring_sprintf(test_label, "%d (last byte stripped)", - (int) (v1_test_case - v1_test_cases)); - mutated_test_case = *v1_test_case; - mutated_test_case.exp_return = "missing protocol header terminator"; - mutated_test_case.haproxy_req_len -= 1; - mutated_test_case.exp_req_len = mutated_test_case.haproxy_req_len; - test_failed += evaluate_test_case(STR(test_label), &mutated_test_case); - - /* - * A 'well-formed' v1 test case should pass after conversion to v2. - */ - vstring_sprintf(test_label, "%d (converted to v2)", - (int) (v1_test_case - v1_test_cases)); - v2_test_case = *v1_test_case; - convert_v1_proxy_req_to_v2(v2_request_buf, - v1_test_case->haproxy_request, - v1_test_case->haproxy_req_len); - v2_test_case.haproxy_request = STR(v2_request_buf); - v2_test_case.haproxy_req_len = PP2_HEADER_LEN - + ntohs(((struct proxy_hdr_v2 *) STR(v2_request_buf))->len); - v2_test_case.exp_req_len = v2_test_case.haproxy_req_len; - test_failed += evaluate_test_case(STR(test_label), &v2_test_case); - - /* - * Mutation test: a well-formed v2 test case should still pass after - * appending a byte, and should return the actual parsed header - * length. The test uses the implicit VSTRING null safety byte. - */ - vstring_sprintf(test_label, "%d (converted to v2, one byte appended)", - (int) (v1_test_case - v1_test_cases)); - mutated_test_case = v2_test_case; - mutated_test_case.haproxy_req_len += 1; - /* reuse v2_test_case->exp_req_len */ - test_failed += evaluate_test_case(STR(test_label), &mutated_test_case); - - /* - * Mutation test: a well-formed v2 test case should fail after - * stripping one byte - */ - vstring_sprintf(test_label, "%d (converted to v2, last byte stripped)", - (int) (v1_test_case - v1_test_cases)); - mutated_test_case = v2_test_case; - mutated_test_case.haproxy_req_len -= 1; - mutated_test_case.exp_req_len = mutated_test_case.haproxy_req_len; - mutated_test_case.exp_return = "short version 2 protocol header"; - test_failed += evaluate_test_case(STR(test_label), &mutated_test_case); - } - - /* - * Additional V2-only tests. - */ - test_failed += - evaluate_test_case("v2 non-proxy request", &v2_non_proxy_test); - - /* - * Clean up. - */ - vstring_free(v2_request_buf); - vstring_free(mutated_request_buf); - vstring_free(test_label); - if (tests_failed) - msg_info("tests failed: %d", tests_failed); - exit(tests_failed != 0); -} - -#endif diff --git a/postfix/src/global/haproxy_srvr.h b/postfix/src/global/haproxy_srvr.h index 4a801f1d8..d2fe60a55 100644 --- a/postfix/src/global/haproxy_srvr.h +++ b/postfix/src/global/haproxy_srvr.h @@ -33,6 +33,79 @@ extern int haproxy_srvr_receive(int, int *, #define DONT_GRIPE 0 #endif + /* + * Binary V2 protocol structure, also needed to create test data. + */ +#ifdef HAPROXY_SRVR_INTERNAL + + /* + * Begin protocol v2 definitions from haproxy/include/types/connection.h. + */ +#define PP2_SIGNATURE "\r\n\r\n\0\r\nQUIT\n" +#define PP2_SIGNATURE_LEN 12 +#define PP2_HEADER_LEN 16 + +/* ver_cmd byte */ +#define PP2_CMD_LOCAL 0x00 +#define PP2_CMD_PROXY 0x01 +#define PP2_CMD_MASK 0x0F + +#define PP2_VERSION 0x20 +#define PP2_VERSION_MASK 0xF0 + +/* fam byte */ +#define PP2_TRANS_UNSPEC 0x00 +#define PP2_TRANS_STREAM 0x01 +#define PP2_TRANS_DGRAM 0x02 +#define PP2_TRANS_MASK 0x0F + +#define PP2_FAM_UNSPEC 0x00 +#define PP2_FAM_INET 0x10 +#define PP2_FAM_INET6 0x20 +#define PP2_FAM_UNIX 0x30 +#define PP2_FAM_MASK 0xF0 + +/* len field (2 bytes) */ +#define PP2_ADDR_LEN_UNSPEC (0) +#define PP2_ADDR_LEN_INET (4 + 4 + 2 + 2) +#define PP2_ADDR_LEN_INET6 (16 + 16 + 2 + 2) +#define PP2_ADDR_LEN_UNIX (108 + 108) + +#define PP2_HDR_LEN_UNSPEC (PP2_HEADER_LEN + PP2_ADDR_LEN_UNSPEC) +#define PP2_HDR_LEN_INET (PP2_HEADER_LEN + PP2_ADDR_LEN_INET) +#define PP2_HDR_LEN_INET6 (PP2_HEADER_LEN + PP2_ADDR_LEN_INET6) +#define PP2_HDR_LEN_UNIX (PP2_HEADER_LEN + PP2_ADDR_LEN_UNIX) + +struct proxy_hdr_v2 { + uint8_t sig[PP2_SIGNATURE_LEN]; /* PP2_SIGNATURE */ + uint8_t ver_cmd; /* protocol version | command */ + uint8_t fam; /* protocol family and transport */ + uint16_t len; /* length of remainder */ + union { + struct { /* for TCP/UDP over IPv4, len = 12 */ + uint32_t src_addr; + uint32_t dst_addr; + uint16_t src_port; + uint16_t dst_port; + } ip4; + struct { /* for TCP/UDP over IPv6, len = 36 */ + uint8_t src_addr[16]; + uint8_t dst_addr[16]; + uint16_t src_port; + uint16_t dst_port; + } ip6; + struct { /* for AF_UNIX sockets, len = 216 */ + uint8_t src_addr[108]; + uint8_t dst_addr[108]; + } unx; + } addr; +}; + + /* + * End protocol v2 definitions from haproxy/include/types/connection.h. + */ +#endif /* HAPROXY_SRVR_INTERNAL */ + /* LICENSE /* .ad /* .fi diff --git a/postfix/src/global/haproxy_srvr_test.c b/postfix/src/global/haproxy_srvr_test.c new file mode 100644 index 000000000..fca1febb7 --- /dev/null +++ b/postfix/src/global/haproxy_srvr_test.c @@ -0,0 +1,396 @@ + /* + * Test program to exercise haproxy_srvr.c. See ptest_main.h for a + * documented example. + */ + + /* + * System library. + */ +#include +#include + + /* + * Utility library. + */ +#include +#include + + /* + * Global library. + */ +#define HAPROXY_SRVR_INTERNAL +#include + + /* + * Test library. + */ +#include + + /* + * Test cases with inputs and expected outputs. A request may contain + * trailing garbage, and it may be too short. A v1 request may also contain + * malformed address or port information. + */ +typedef struct BASE_TEST_CASE { + const char *haproxy_request; /* v1 or v2 request including thrash */ + ssize_t haproxy_req_len; /* request length including thrash */ + ssize_t want_req_len; /* parsed request length */ + int want_non_proxy; /* request is not proxied */ + const char *want_return; /* expected error string */ + const char *want_client_addr; /* expected client address string */ + const char *want_server_addr; /* expected client port string */ + const char *want_client_port; /* expected client address string */ + const char *want_server_port; /* expected server port string */ +} BASE_TEST_CASE; + + /* + * Initialize the haproxy_request, haproxy_req_len, and want_req_len + * fields. + */ +#define STRING_AND_LENS(s) (s), (sizeof(s) - 1), (sizeof(s) - 1) + + /* + * This table contains V1 protocol test cases that may also be converted + * into V2 protocol test cases. + */ +static BASE_TEST_CASE v1_test_cases[] = { + /* IPv6. */ + {STRING_AND_LENS("PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1 123 321\n"), 0, 0, "fc::1:2:3:4", "fc::4:3:2:1", "123", "321"}, + {STRING_AND_LENS("PROXY TCP6 FC:00:00:00:1:2:3:4 FC:00:00:00:4:3:2:1 123 321\n"), 0, 0, "fc::1:2:3:4", "fc::4:3:2:1", "123", "321"}, + {STRING_AND_LENS("PROXY TCP6 1.2.3.4 4.3.2.1 123 321\n"), 0, "bad or missing client address"}, + {STRING_AND_LENS("PROXY TCP6 fc:00:00:00:1:2:3:4 4.3.2.1 123 321\n"), 0, "bad or missing server address"}, + /* IPv4 in IPv6. */ + {STRING_AND_LENS("PROXY TCP6 ::ffff:1.2.3.4 ::ffff:4.3.2.1 123 321\n"), 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"}, + {STRING_AND_LENS("PROXY TCP6 ::FFFF:1.2.3.4 ::FFFF:4.3.2.1 123 321\n"), 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"}, + {STRING_AND_LENS("PROXY TCP4 ::ffff:1.2.3.4 ::ffff:4.3.2.1 123 321\n"), 0, "bad or missing client address"}, + {STRING_AND_LENS("PROXY TCP4 1.2.3.4 ::ffff:4.3.2.1 123 321\n"), 0, "bad or missing server address"}, + /* IPv4. */ + {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 123 321\n"), 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"}, + {STRING_AND_LENS("PROXY TCP4 01.02.03.04 04.03.02.01 123 321\n"), 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"}, + {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 123456 321\n"), 0, "bad or missing client port"}, + {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 123 654321\n"), 0, "bad or missing server port"}, + {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 0123 321\n"), 0, "bad or missing client port"}, + {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 123 0321\n"), 0, "bad or missing server port"}, + /* Missing fields. */ + {STRING_AND_LENS("PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1 123\n"), 0, "bad or missing server port"}, + {STRING_AND_LENS("PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1\n"), 0, "bad or missing client port"}, + {STRING_AND_LENS("PROXY TCP6 fc:00:00:00:1:2:3:4\n"), 0, "bad or missing server address"}, + {STRING_AND_LENS("PROXY TCP6\n"), 0, "bad or missing client address"}, + {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 123\n"), 0, "bad or missing server port"}, + {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1\n"), 0, "bad or missing client port"}, + {STRING_AND_LENS("PROXY TCP4 1.2.3.4\n"), 0, "bad or missing server address"}, + {STRING_AND_LENS("PROXY TCP4\n"), 0, "bad or missing client address"}, + /* Other. */ + {STRING_AND_LENS("PROXY BLAH\n"), 0, "bad or missing protocol type"}, + {STRING_AND_LENS("PROXY\n"), 0, "short protocol header"}, + {"BLAH\n", 0, 0, 0, "short protocol header"}, + {"\n", 0, 0, 0, "short protocol header"}, + {"", 0, 0, 0, "short protocol header"}, + 0, +}; + + /* + * Limited V2-only tests. XXX Should we add error cases? Several errors are + * already tested with mutations of V2 handshakes that were generated from + * V1 handshakes. + */ +static struct proxy_hdr_v2 v2_local_request = { + PP2_SIGNATURE, PP2_VERSION | PP2_CMD_LOCAL, +}; +static BASE_TEST_CASE v2_non_proxy_test = { + (char *) &v2_local_request, PP2_HEADER_LEN, PP2_HEADER_LEN, 1, +}; + +#define STR_OR_NULL(s) ((s) ? (s) : "(null)") +#define STR(x) vstring_str(x) +#define LEN(x) VSTRING_LEN(x) + +/* evaluate_test_case - evaluate one test case (base or mutation) */ + +static void evaluate_test_case(PTEST_CTX *t,const char *test_label, + const BASE_TEST_CASE *test_case) +{ + PTEST_RUN(t, test_label, { + const char *got_return; + ssize_t got_req_len; + int got_non_proxy; + MAI_HOSTADDR_STR got_smtp_client_addr; + MAI_HOSTADDR_STR got_smtp_server_addr; + MAI_SERVPORT_STR got_smtp_client_port; + MAI_SERVPORT_STR got_smtp_server_port; + + if (msg_verbose) + msg_info("test case=%s want_client_addr=%s want_server_addr=%s " + "want_client_port=%s want_server_port=%s", + test_label, STR_OR_NULL(test_case->want_client_addr), + STR_OR_NULL(test_case->want_server_addr), + STR_OR_NULL(test_case->want_client_port), + STR_OR_NULL(test_case->want_server_port)); + + /* + * Start the test. + */ + got_req_len = test_case->haproxy_req_len; + got_return = + haproxy_srvr_parse(test_case->haproxy_request, &got_req_len, + &got_non_proxy, + &got_smtp_client_addr, &got_smtp_client_port, + &got_smtp_server_addr, &got_smtp_server_port); + if (strcmp(STR_OR_NULL(got_return), STR_OR_NULL(test_case->want_return))) { + ptest_error(t, "haproxy_srvr_parse return got=%s want=%s", + STR_OR_NULL(got_return), + STR_OR_NULL(test_case->want_return)); + ptest_return(t); + } + if (got_req_len != test_case->want_req_len) { + ptest_error(t, "haproxy_srvr_parse str_len got=%ld want=%ld", + (long) got_req_len, + (long) test_case->want_req_len); + ptest_return(t); + } + if (got_non_proxy != test_case->want_non_proxy) { + ptest_error(t, "haproxy_srvr_parse non_proxy got=%d want=%d", + got_non_proxy, + test_case->want_non_proxy); + ptest_return(t); + } + if (test_case->want_non_proxy || test_case->want_return != 0) + /* No expected address/port results. */ + ptest_return(t); + + /* + * Compare address/port results against expected results. + */ + if (strcmp(test_case->want_client_addr, got_smtp_client_addr.buf)) { + ptest_error(t, "haproxy_srvr_parse client_addr got=%s want=%s", + got_smtp_client_addr.buf, + test_case->want_client_addr); + } + if (strcmp(test_case->want_server_addr, got_smtp_server_addr.buf)) { + ptest_error(t, "haproxy_srvr_parse server_addr got=%s want=%s", + got_smtp_server_addr.buf, + test_case->want_server_addr); + } + if (strcmp(test_case->want_client_port, got_smtp_client_port.buf)) { + ptest_error(t, "haproxy_srvr_parse client_port got=%s want=%s", + got_smtp_client_port.buf, + test_case->want_client_port); + } + if (strcmp(test_case->want_server_port, got_smtp_server_port.buf)) { + ptest_error(t, "haproxy_srvr_parse server_port got=%s want=%s", + got_smtp_server_port.buf, + test_case->want_server_port); + } + }); +} + +/* convert_v1_proxy_req_to_v2 - convert well-formed v1 proxy request to v2 */ + +static void convert_v1_proxy_req_to_v2(PTEST_CTX * t, VSTRING *buf, + const char *req, ssize_t req_len) +{ + const char myname[] = "convert_v1_proxy_req_to_v2"; + const char *err; + int non_proxy; + MAI_HOSTADDR_STR smtp_client_addr; + MAI_SERVPORT_STR smtp_client_port; + MAI_HOSTADDR_STR smtp_server_addr; + MAI_SERVPORT_STR smtp_server_port; + struct proxy_hdr_v2 *hdr_v2; + struct addrinfo *src_res; + struct addrinfo *dst_res; + + /* + * Allocate buffer space for the largest possible protocol header, so we + * don't have to worry about hidden realloc() calls. + */ + VSTRING_RESET(buf); + VSTRING_SPACE(buf, sizeof(struct proxy_hdr_v2)); + hdr_v2 = (struct proxy_hdr_v2 *) STR(buf); + + /* + * Fill in the header, + */ + memcpy(hdr_v2->sig, PP2_SIGNATURE, PP2_SIGNATURE_LEN); + hdr_v2->ver_cmd = PP2_VERSION | PP2_CMD_PROXY; + if ((err = haproxy_srvr_parse(req, &req_len, &non_proxy, &smtp_client_addr, + &smtp_client_port, &smtp_server_addr, + &smtp_server_port)) != 0 || non_proxy) + ptest_fatal(t, "%s: malformed or non-proxy request: %s", + myname, req); + + if (hostaddr_to_sockaddr(smtp_client_addr.buf, smtp_client_port.buf, 0, + &src_res) != 0) + ptest_fatal(t, "%s: unable to convert source address %s port %s", + myname, smtp_client_addr.buf, smtp_client_port.buf); + if (hostaddr_to_sockaddr(smtp_server_addr.buf, smtp_server_port.buf, 0, + &dst_res) != 0) + ptest_fatal(t, "%s: unable to convert destination address %s port %s", + myname, smtp_server_addr.buf, smtp_server_port.buf); + if (src_res->ai_family != dst_res->ai_family) + ptest_fatal(t, "%s: mixed source/destination address families", myname); +#ifdef AF_INET6 + if (src_res->ai_family == PF_INET6) { + hdr_v2->fam = PP2_FAM_INET6 | PP2_TRANS_STREAM; + hdr_v2->len = htons(PP2_ADDR_LEN_INET6); + memcpy(hdr_v2->addr.ip6.src_addr, + &SOCK_ADDR_IN6_ADDR(src_res->ai_addr), + sizeof(hdr_v2->addr.ip6.src_addr)); + hdr_v2->addr.ip6.src_port = SOCK_ADDR_IN6_PORT(src_res->ai_addr); + memcpy(hdr_v2->addr.ip6.dst_addr, + &SOCK_ADDR_IN6_ADDR(dst_res->ai_addr), + sizeof(hdr_v2->addr.ip6.dst_addr)); + hdr_v2->addr.ip6.dst_port = SOCK_ADDR_IN6_PORT(dst_res->ai_addr); + } else +#endif + if (src_res->ai_family == PF_INET) { + hdr_v2->fam = PP2_FAM_INET | PP2_TRANS_STREAM; + hdr_v2->len = htons(PP2_ADDR_LEN_INET); + hdr_v2->addr.ip4.src_addr = SOCK_ADDR_IN_ADDR(src_res->ai_addr).s_addr; + hdr_v2->addr.ip4.src_port = SOCK_ADDR_IN_PORT(src_res->ai_addr); + hdr_v2->addr.ip4.dst_addr = SOCK_ADDR_IN_ADDR(dst_res->ai_addr).s_addr; + hdr_v2->addr.ip4.dst_port = SOCK_ADDR_IN_PORT(dst_res->ai_addr); + } else { + ptest_fatal(t, "unknown address family 0x%x", src_res->ai_family); + } + vstring_set_payload_size(buf, PP2_SIGNATURE_LEN + ntohs(hdr_v2->len)); + freeaddrinfo(src_res); + freeaddrinfo(dst_res); +} + +static void run_test_variants(PTEST_CTX *t, BASE_TEST_CASE *v1_test_case) +{ + VSTRING *test_label; + BASE_TEST_CASE v2_test_case; + BASE_TEST_CASE mutated_test_case; + VSTRING *v2_request_buf; + VSTRING *mutated_request_buf; + + test_label = vstring_alloc(100); + v2_request_buf = vstring_alloc(100); + mutated_request_buf = vstring_alloc(100); + + /* + * Evaluate each v1 test case. + */ + vstring_sprintf(test_label, "v1 baseline (%sformed)", + v1_test_case->want_return ? "mal" : "well"); + evaluate_test_case(t, STR(test_label), v1_test_case); + + /* + * If the v1 test input is malformed, skip the mutation tests. + */ + if (v1_test_case->want_return != 0) + ptest_return(t); + + /* + * Mutation test: a well-formed v1 test case should still pass after + * appending a byte, and should return the actual parsed header length. + * The test uses the implicit VSTRING null safety byte. + */ + vstring_sprintf(test_label, "v1 mutated (one byte appended)"); + mutated_test_case = *v1_test_case; + mutated_test_case.haproxy_req_len += 1; + /* reuse v1_test_case->want_req_len */ + evaluate_test_case(t, STR(test_label), &mutated_test_case); + + /* + * Mutation test: a well-formed v1 test case should fail after stripping + * the terminator. + */ + vstring_sprintf(test_label, "v1 mutated (last byte stripped)"); + mutated_test_case = *v1_test_case; + mutated_test_case.want_return = "missing protocol header terminator"; + mutated_test_case.haproxy_req_len -= 1; + mutated_test_case.want_req_len = mutated_test_case.haproxy_req_len; + evaluate_test_case(t, STR(test_label), &mutated_test_case); + + /* + * A 'well-formed' v1 test case should pass after conversion to v2. + */ + vstring_sprintf(test_label, "v2 baseline (converted from v1)"); + v2_test_case = *v1_test_case; + convert_v1_proxy_req_to_v2(t, v2_request_buf, + v1_test_case->haproxy_request, + v1_test_case->haproxy_req_len); + v2_test_case.haproxy_request = STR(v2_request_buf); + v2_test_case.haproxy_req_len = PP2_HEADER_LEN + + ntohs(((struct proxy_hdr_v2 *) STR(v2_request_buf))->len); + v2_test_case.want_req_len = v2_test_case.haproxy_req_len; + evaluate_test_case(t, STR(test_label), &v2_test_case); + + /* + * Mutation test: a well-formed v2 test case should still pass after + * appending a byte, and should return the actual parsed header length. + * The test uses the implicit VSTRING null safety byte. + */ + vstring_sprintf(test_label, "v2 mutated (one byte appended)"); + mutated_test_case = v2_test_case; + mutated_test_case.haproxy_req_len += 1; + /* reuse v2_test_case->want_req_len */ + evaluate_test_case(t, STR(test_label), &mutated_test_case); + + /* + * Mutation test: a well-formed v2 test case should fail after stripping + * one byte + */ + vstring_sprintf(test_label, "v2 mutated (last byte stripped)"); + mutated_test_case = v2_test_case; + mutated_test_case.haproxy_req_len -= 1; + mutated_test_case.want_req_len = mutated_test_case.haproxy_req_len; + mutated_test_case.want_return = "short version 2 protocol header"; + evaluate_test_case(t, STR(test_label), &mutated_test_case); + + /* + * Clean up. + */ + vstring_free(v2_request_buf); + vstring_free(mutated_request_buf); + vstring_free(test_label); +} + + /* + * PTEST adapter. + */ +typedef struct PTEST_CASE { + const char *testname; + void (*action) (PTEST_CTX *, const struct PTEST_CASE *); +} PTEST_CASE; + +static void run_proxy_tests(PTEST_CTX *t, const PTEST_CASE *unused) +{ + BASE_TEST_CASE *v1_test_case; + VSTRING *test_label; + + /* + * Run all variants of one base case test together in a subtest. + */ + test_label = vstring_alloc(100); + for (v1_test_case = v1_test_cases; + v1_test_case->haproxy_request != 0; v1_test_case++) { + vstring_sprintf(test_label, "%d", + 1 + (int) (v1_test_case - v1_test_cases)); + PTEST_RUN(t, STR(test_label), { + run_test_variants(t, v1_test_case); + }); + } + + /* + * Additional V2-only test. + */ + vstring_sprintf(test_label, "%d", + 1 + (int) (v1_test_case - v1_test_cases)); + PTEST_RUN(t, STR(test_label), { + evaluate_test_case(t, "v2 non-proxy request", &v2_non_proxy_test); + }); + vstring_free(test_label); +} + + /* + * PTEST adapter. + */ +static const PTEST_CASE ptestcases[] = { + "haproxy_srvr_test", run_proxy_tests, +}; + +#include diff --git a/postfix/src/global/mail_proto.h b/postfix/src/global/mail_proto.h index c5f59c2d7..315a2e15d 100644 --- a/postfix/src/global/mail_proto.h +++ b/postfix/src/global/mail_proto.h @@ -141,7 +141,7 @@ extern char *mail_pathname(const char *, const char *); #define MAIL_ATTR_PROTO_VERIFY "address_verification_prrotocol" /* - * Attribute names. + * Attribute names in internal and policy delegation protocols. */ #define MAIL_ATTR_REQ "request" #define MAIL_ATTR_NREQ "nrequest" @@ -201,6 +201,7 @@ extern char *mail_pathname(const char *, const char *); #define MAIL_ATTR_CRYPTO_CIPHER "encryption_cipher" #define MAIL_ATTR_CRYPTO_KEYSIZE "encryption_keysize" #define MAIL_ATTR_COMPAT_LEVEL "compatibility_level" +#define MAIL_ATTR_MAIL_VERSION "mail_version" /* * Suffixes for sender_name, sender_domain etc. diff --git a/postfix/src/global/mail_version.h b/postfix/src/global/mail_version.h index 49f073faa..820708a2d 100644 --- a/postfix/src/global/mail_version.h +++ b/postfix/src/global/mail_version.h @@ -20,7 +20,7 @@ * Patches change both the patchlevel and the release date. Snapshots have no * patchlevel; they change the release date only. */ -#define MAIL_RELEASE_DATE "20220724" +#define MAIL_RELEASE_DATE "20220816" #define MAIL_VERSION_NUMBER "3.8" #ifdef SNAPSHOT diff --git a/postfix/src/global/map_search_test.c b/postfix/src/global/map_search_test.c index ef8f88c7c..974443adf 100644 --- a/postfix/src/global/map_search_test.c +++ b/postfix/src/global/map_search_test.c @@ -128,7 +128,6 @@ static void test_map_search(PTEST_CTX *t, const struct PTEST_CASE *unused) for (tp = test_cases; tp->map_spec; tp++) { vstring_sprintf(test_label, "test %d", (int) (tp - test_cases)); -/**INDENT** Error@126: Unbalanced parens */ PTEST_RUN(t, STR(test_label), { for (cpp = tp->want_log; cpp < tp->want_log + MAX_WANT_LOG && *cpp; cpp++) expect_ptest_log_event(t, *cpp); @@ -171,7 +170,6 @@ static void test_map_search(PTEST_CTX *t, const struct PTEST_CASE *unused) escape_order(want_escaped, string_or_null(tp->exp_search_order))); } -/**INDENT** Warning@168: Extra ) */ }); } vstring_free(want_escaped); diff --git a/postfix/src/postscreen/Makefile.in b/postfix/src/postscreen/Makefile.in index 034c01be7..509e7ab13 100644 --- a/postfix/src/postscreen/Makefile.in +++ b/postfix/src/postscreen/Makefile.in @@ -10,11 +10,12 @@ OBJS = postscreen.o postscreen_dict.o postscreen_dnsbl.o \ postscreen_starttls.o postscreen_expand.o postscreen_endpt.o \ postscreen_haproxy.o HDRS = -TESTSRC = +TESTSRC = postscreen_dnsbl_test.c DEFS = -I. -I$(INC_DIR) -D$(SYSTYPE) CFLAGS = $(DEBUG) $(OPT) $(DEFS) -TESTPROG= +TESTPROG= postscreen_dnsbl_test PROG = postscreen +TEST_LIB= ../../lib/libtesting.a ../../lib/libptest.a INC_DIR = ../../include LIBS = ../../lib/lib$(LIB_PREFIX)master$(LIB_SUFFIX) \ ../../lib/lib$(LIB_PREFIX)tls$(LIB_SUFFIX) \ @@ -34,7 +35,7 @@ Makefile: Makefile.in test: $(TESTPROG) -tests: test +tests: test_postscreen_dnsbl root_tests: @@ -59,6 +60,14 @@ clean: tidy: clean +postscreen_dnsbl_test: postscreen_dnsbl_test.o postscreen_dnsbl.o \ + ../../lib//mock_server.o $(TEST_LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.o postscreen_dnsbl.o \ + ../../lib//mock_server.o $(TEST_LIB) $(LIBS) $(SYSLIBS) + +test_postscreen_dnsbl: update postscreen_dnsbl_test + $(SHLIB_ENV) ${VALGRIND} ./postscreen_dnsbl_test + depend: $(MAKES) (sed '1,/^# do not edit/!d' Makefile.in; \ set -e; for i in [a-z][a-z0-9]*.c; do \ @@ -156,6 +165,42 @@ postscreen_dnsbl.o: ../../include/vstring.h postscreen_dnsbl.o: ../../include/wrap_netdb.h postscreen_dnsbl.o: postscreen.h postscreen_dnsbl.o: postscreen_dnsbl.c +postscreen_dnsbl_test.o: ../../include/addr_match_list.h +postscreen_dnsbl_test.o: ../../include/argv.h +postscreen_dnsbl_test.o: ../../include/attr.h +postscreen_dnsbl_test.o: ../../include/check_arg.h +postscreen_dnsbl_test.o: ../../include/connect.h +postscreen_dnsbl_test.o: ../../include/dict.h +postscreen_dnsbl_test.o: ../../include/dict_cache.h +postscreen_dnsbl_test.o: ../../include/events.h +postscreen_dnsbl_test.o: ../../include/htable.h +postscreen_dnsbl_test.o: ../../include/iostuff.h +postscreen_dnsbl_test.o: ../../include/mail_params.h +postscreen_dnsbl_test.o: ../../include/mail_proto.h +postscreen_dnsbl_test.o: ../../include/make_attr.h +postscreen_dnsbl_test.o: ../../include/maps.h +postscreen_dnsbl_test.o: ../../include/match_list.h +postscreen_dnsbl_test.o: ../../include/mock_server.h +postscreen_dnsbl_test.o: ../../include/msg.h +postscreen_dnsbl_test.o: ../../include/msg_output.h +postscreen_dnsbl_test.o: ../../include/msg_vstream.h +postscreen_dnsbl_test.o: ../../include/myaddrinfo.h +postscreen_dnsbl_test.o: ../../include/myflock.h +postscreen_dnsbl_test.o: ../../include/mymalloc.h +postscreen_dnsbl_test.o: ../../include/nvtable.h +postscreen_dnsbl_test.o: ../../include/pmock_expect.h +postscreen_dnsbl_test.o: ../../include/ptest.h +postscreen_dnsbl_test.o: ../../include/ptest_main.h +postscreen_dnsbl_test.o: ../../include/server_acl.h +postscreen_dnsbl_test.o: ../../include/string_list.h +postscreen_dnsbl_test.o: ../../include/stringops.h +postscreen_dnsbl_test.o: ../../include/sys_defs.h +postscreen_dnsbl_test.o: ../../include/vbuf.h +postscreen_dnsbl_test.o: ../../include/vstream.h +postscreen_dnsbl_test.o: ../../include/vstring.h +postscreen_dnsbl_test.o: ../../include/wrap_netdb.h +postscreen_dnsbl_test.o: postscreen.h +postscreen_dnsbl_test.o: postscreen_dnsbl_test.c postscreen_early.o: ../../include/addr_match_list.h postscreen_early.o: ../../include/argv.h postscreen_early.o: ../../include/check_arg.h diff --git a/postfix/src/postscreen/postscreen.h b/postfix/src/postscreen/postscreen.h index 69a5e1750..b9ceeeb99 100644 --- a/postfix/src/postscreen/postscreen.h +++ b/postfix/src/postscreen/postscreen.h @@ -485,6 +485,7 @@ const char *psc_maps_find(MAPS *, const char *, int); extern void psc_dnsbl_init(void); extern int psc_dnsbl_retrieve(const char *, const char **, int, int *); extern int psc_dnsbl_request(const char *, void (*) (int, void *), void *); +extern void psc_dnsbl_deinit(void); /* * postscreen_tests.c diff --git a/postfix/src/postscreen/postscreen_dnsbl.c b/postfix/src/postscreen/postscreen_dnsbl.c index 7d9a5e94b..c726f5478 100644 --- a/postfix/src/postscreen/postscreen_dnsbl.c +++ b/postfix/src/postscreen/postscreen_dnsbl.c @@ -15,10 +15,12 @@ /* /* int psc_dnsbl_retrieve(client_addr, dnsbl_name, dnsbl_index, /* dnsbl_ttl) -/* char *client_addr; +/* const char *client_addr; /* const char **dnsbl_name; /* int dnsbl_index; /* int *dnsbl_ttl; +/* AUXILIARY FUNCTIONS +/* void psc_dnsbl_deinit(void) /* DESCRIPTION /* This module implements preliminary support for DNSBL lookups. /* Multiple requests for the same information are handled with @@ -44,6 +46,9 @@ /* reference count. The reply TTL value is clamped to /* postscreen_dnsbl_min_ttl and postscreen_dnsbl_max_ttl. It /* is an error to retrieve a score without requesting it first. +/* +/* psc_dnsbl_deinit() tries to reset state so that psc_dnsbl_init() +/* can be called again. This is to support tests only. /* LICENSE /* .ad /* .fi @@ -115,10 +120,23 @@ static HTABLE *dnsbl_site_cache; /* indexed by DNSBNL domain */ static HTABLE_INFO **dnsbl_site_list; /* flattened cache */ typedef struct { - const char *safe_dnsbl; /* from postscreen_dnsbl_reply_map */ + char *safe_dnsbl; /* from postscreen_dnsbl_reply_map */ struct PSC_DNSBL_SITE *first; /* list of (filter, weight) tuples */ } PSC_DNSBL_HEAD; +static void psc_dnsbl_site_free(void *ptr); + +static void psc_dnsbl_head_free(void *ptr) +{ + PSC_DNSBL_HEAD *head = (PSC_DNSBL_HEAD *) ptr; + + if (head->safe_dnsbl) + myfree(head->safe_dnsbl); + if (head->first) + psc_dnsbl_site_free(head->first); + myfree(head); +}; + typedef struct PSC_DNSBL_SITE { char *filter; /* printable filter (default: null) */ char *byte_codes; /* encoded filter (default: null) */ @@ -126,6 +144,19 @@ typedef struct PSC_DNSBL_SITE { struct PSC_DNSBL_SITE *next; /* linked list */ } PSC_DNSBL_SITE; +static void psc_dnsbl_site_free(void *ptr) +{ + PSC_DNSBL_SITE *site = (PSC_DNSBL_SITE *) ptr; + + if (site->filter) + myfree(site->filter); + if (site->byte_codes) + myfree(site->byte_codes); + if (site->next) + psc_dnsbl_site_free(site->next); + myfree(site); +} + /* * Per-client DNSBL scores. * @@ -162,6 +193,13 @@ typedef struct { PSC_CALL_BACK_ENTRY table[1]; /* actually a bunch */ } PSC_DNSBL_SCORE; +static void psc_dnsbl_score_free(void *ptr) +{ + PSC_DNSBL_SCORE *score = (PSC_DNSBL_SCORE *) ptr; + + myfree(score); +} + #define PSC_CALL_BACK_INIT(sp) do { \ (sp)->limit = 0; \ (sp)->index = 0; \ @@ -197,8 +235,10 @@ typedef struct { } while (0) #define PSC_CALL_BACK_NOTIFY(sp, ev) do { \ + PSC_CALL_BACK_ENTRY *_table_ = (sp)->table; \ + int _index_ = (sp)->index; \ PSC_CALL_BACK_ENTRY *_cb_; \ - for (_cb_ = (sp)->table; _cb_ < (sp)->table + (sp)->index; _cb_++) \ + for (_cb_ = _table_; _cb_ < _table_ + _index_; _cb_++) \ if (_cb_->callback != 0) \ _cb_->callback((ev), _cb_->context); \ } while (0) @@ -231,7 +271,7 @@ static void psc_dnsbl_add_site(const char *site) int weight; HTABLE_INFO *ht; char *parse_err; - const char *safe_dnsbl; + const char *safe_dnsbl; /* * Parse the required DNSBL domain name, the optional reply filter and @@ -480,6 +520,8 @@ static void psc_dnsbl_receive(int event, void *context) vstream_fclose(stream); } +static int request_count; + /* psc_dnsbl_request - send dnsbl query, increment reference count */ int psc_dnsbl_request(const char *client_addr, @@ -492,7 +534,6 @@ int psc_dnsbl_request(const char *client_addr, HTABLE_INFO **ht; PSC_DNSBL_SCORE *score; HTABLE_INFO *hash_node; - static int request_count; /* * Some spambots make several connections at nearly the same time, @@ -621,4 +662,42 @@ void psc_dnsbl_init(void) reply_client = vstring_alloc(100); reply_dnsbl = vstring_alloc(100); reply_addr = vstring_alloc(100); + + /* + * Reset the request ID seed, to make tests predictable. + */ + request_count = 0; +} + +/* psc_dnsbl_deinit - helper for tests only */ + +void psc_dnsbl_deinit(void) +{ + if (psc_dnsbl_service) { + myfree(psc_dnsbl_service); + psc_dnsbl_service = 0; + } + if (dnsbl_site_cache) { + htable_free(dnsbl_site_cache, psc_dnsbl_head_free); + dnsbl_site_cache = 0; + } + if (dnsbl_site_list) { + myfree(dnsbl_site_list); + dnsbl_site_list = 0; + } + if (dnsbl_score_cache) { + htable_free(dnsbl_score_cache, psc_dnsbl_score_free); + dnsbl_score_cache = 0; + } + if (reply_client) { + vstring_free(reply_client); + reply_client = 0; + } + if (reply_dnsbl) { + vstring_free(reply_dnsbl), reply_dnsbl = 0; + } + if (reply_addr) { + vstring_free(reply_addr); + reply_addr = 0; + } } diff --git a/postfix/src/postscreen/postscreen_dnsbl_test.c b/postfix/src/postscreen/postscreen_dnsbl_test.c new file mode 100644 index 000000000..71a275ec2 --- /dev/null +++ b/postfix/src/postscreen/postscreen_dnsbl_test.c @@ -0,0 +1,563 @@ + /* + * Test program to exercise postscreen_dnsbl.c. See comments in + * mock_server.c, and PTEST_README for documented examples of unit tests. + */ + + /* + * System library. + */ +#include +#include + + /* + * Utility library. + */ +#include +#include +#include +#include + + /* + * Global library. + */ +#include +#include + + /* + * Test library. + */ +#include +#include +#include + + /* + * Application-specific. + */ +#include + + /* + * Generic case structure. + */ +typedef struct PTEST_CASE { + const char *testname; /* Human-readable description */ + void (*action) (PTEST_CTX *t, const struct PTEST_CASE *); +} PTEST_CASE; + + /* + * Structure to capture postscreen_dnsbl_retrieve() inputs and outputs. + */ +struct session_state { + /* postscreen_dnsbl_retrieve() inputs. */ + const char *req_addr; /* Client IP address */ + int req_idx; /* Request index */ + /* postscreen_dnsbl_retrieve() outputs. */ + const char *got_dnsbl; /* Null, or biggest contributor */ + int got_ttl; /* TTL from A or SOA record */ + int got_score; /* Combined score */ +}; + + /* + * Surrogates for global variables used, but not defined, by + * postscreen_dnsbl.c. + */ +int var_psc_dnsbl_min_ttl; /* postscreen_dnsbl_min_ttl */ +int var_psc_dnsbl_max_ttl; /* postscreen_dnsbl_max_ttl */ +int var_psc_dnsbl_tmout; /* postscreen_dnsbl_timeout */ +char *var_psc_dnsbl_sites; /* postscreen_dnsbl_sites */ +char *var_dnsblog_service; /* dnsblog_service_name */ +DICT *psc_dnsbl_reply; /* postscreen_dnsbl_reply_map */ + +/* deinit_psc_globals - best effort reset */ + +static void deinit_psc_globals(void) +{ + + /* + * deinit_psc_globals() must be idempotent, so that it can be called + * safely at the start and end of each test. + */ + if (var_psc_dnsbl_sites) { + myfree(var_psc_dnsbl_sites); + var_psc_dnsbl_sites = 0; + } + if (psc_dnsbl_reply) { + dict_close(psc_dnsbl_reply); + psc_dnsbl_reply = 0; + } + + /* + * Reset postscreen_dnsbl.c internals. + */ + psc_dnsbl_deinit(); +} + +/* init_psc_globals - initialize globals */ + +static void init_psc_globals(const char *dnsbl_sites) +{ + + /* + * We call deinit_psc_globals() first, because it may not be called at + * the end of a failed test. A test failure should not affect later + * tests. + */ + deinit_psc_globals(); + + /* + * Set parameters that postscreen_dnsbl.c depends on. + */ + var_psc_dnsbl_min_ttl = 60; + var_psc_dnsbl_max_ttl = 3600; + var_psc_dnsbl_tmout = atoi(DEF_PSC_DNSBL_TMOUT); + var_psc_dnsbl_sites = mystrdup(dnsbl_sites); + var_dnsblog_service = DEF_DNSBLOG_SERVICE; + + /* + * postscreen_dnsbl.c mandatory initialization. + */ + psc_dnsbl_init(); +} + +/* psc_dnsbl_callback - event handler to retrieve score and ttl */ + +static void psc_dnsbl_callback(int event, void *context) +{ + struct session_state *sp = (struct session_state *) context; + + sp->got_score = psc_dnsbl_retrieve(sp->req_addr, &sp->got_dnsbl, + sp->req_idx, &sp->got_ttl); +} + + /* + * Test data and tests for a single reputation provider. + */ +struct single_dnsbl_data { + const char *label; /* test label */ + const char *dnsbl_sites; /* postscreen_dnsbl_sites */ + const char *req_dnsbl; /* in dnsblog request */ + const char *req_addr; /* in dnsblog request */ + const char *res_addr; /* in dnsblog response */ + int res_ttl; /* in dnsblog response */ + int want_score; /* sum of weights */ +}; + +static const struct single_dnsbl_data single_dnsbl_tests[] = { + { + "single site listed address", + /* dnsbl_sites */ "zen.spamhaus.org", + /* req_dnsbl */ "zen.spamhaus.org", + /* req_addr */ "127.0.0.2", + /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10", + /* res_ttl */ 60, + /* want_score */ 1, + }, + { + "repeated site 1x rpc 2x score", + /* dnsbl_sites */ "zen.spamhaus.org, zen.spamhaus.org", + /* req_dnsbl */ "zen.spamhaus.org", + /* req_addr */ "127.0.0.2", + /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10", + /* res_ttl */ 60, + /* want_score */ 2, + }, + { + "unlisted address zero score", + /* dnsbl_sites */ "zen.spamhaus.org", + /* req_dnsbl */ "zen.spamhaus.org", + /* req_addr */ "127.0.0.1", + /* res_addr */ "", + /* res_ttl */ 60, + /* want_score */ 0, + }, + { + "site with weight first", + /* dnsbl_sites */ "zen.spamhaus.org*3, zen.spamhaus.org", + /* req_dnsbl */ "zen.spamhaus.org", + /* req_addr */ "127.0.0.2", + /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10", + /* res_ttl */ 60, + /* want_score */ 4, + }, + { + "site with weight last", + /* dnsbl_sites */ "zen.spamhaus.org, zen.spamhaus.org*3", + /* req_dnsbl */ "zen.spamhaus.org", + /* req_addr */ "127.0.0.2", + /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10", + /* res_ttl */ 60, + /* want_score */ 4, + }, + { + "site with filter+weight first", + /* dnsbl_sites */ "zen.spamhaus.org=127.0.0.10*3, zen.spamhaus.org", + /* req_dnsbl */ "zen.spamhaus.org", + /* req_addr */ "127.0.0.2", + /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10", + /* res_ttl */ 60, + /* want_score */ 4, + }, + { + "site with filter+weight last", + /* dnsbl_sites */ "zen.spamhaus.org, zen.spamhaus.org=127.0.0.10*3", + /* req_dnsbl */ "zen.spamhaus.org", + /* req_addr */ "127.0.0.2", + /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10", + /* res_ttl */ 60, + /* want_score */ 4, + }, + { + "filter+weight add and subtract", + /* dnsbl_sites */ "zen.spamhaus.org=127.0.0.[1..255]*3, zen.spamhaus.org=127.0.0.3*-1", + /* req_dnsbl */ "zen.spamhaus.org", + /* req_addr */ "10.2.3.4", + /* res_addr */ "127.0.0.3 127.0.0.10", + /* res_ttl */ 60, + /* want_score */ 2, + }, + { + "filter+weight add and not subtract", + /* dnsbl_sites */ "zen.spamhaus.org=127.0.0.[1..255]*3, zen.spamhaus.org=127.0.0.3*-1", + /* req_dnsbl */ "zen.spamhaus.org", + /* req_addr */ "10.2.3.4", + /* res_addr */ "127.0.0.10", + /* res_ttl */ 60, + /* want_score */ 3, + }, +}; + +static void test_single_dnsbl(PTEST_CTX *t, const PTEST_CASE *tp) +{ + MOCK_SERVER *mp; + struct session_state session_state; + const char *dnsblog_path = "private/dnsblog"; + VSTRING *serialized_req; + VSTRING *serialized_resp; + const int request_id = 0; + const struct single_dnsbl_data *tt; + + for (tt = single_dnsbl_tests; tt < single_dnsbl_tests + + PTEST_NROF(single_dnsbl_tests); tt++) { + if (tt->label == 0) + ptest_fatal(t, "Null test label in single_dnsbl_tests array!"); + PTEST_RUN(t, tt->label, { + + /* + * Reset global state and parameters used by postscreen_dnsbl.c. + */ + init_psc_globals(tt->dnsbl_sites); + + /* + * Instantiate a mock server. + */ + mp = mock_unix_server_create(dnsblog_path); + + /* + * Set up the expected dnsblog request, and the corresponding + * response. The mock dnsblog server immediately generates a read + * event request, so we should send something soon. + */ + serialized_req = + make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, tt->req_dnsbl), + SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, + tt->req_addr), + SEND_ATTR_INT(MAIL_ATTR_LABEL, request_id), + ATTR_TYPE_END); + serialized_resp = + make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, tt->req_dnsbl), + SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, + tt->req_addr), + SEND_ATTR_INT(MAIL_ATTR_LABEL, request_id), + SEND_ATTR_STR(MAIL_ATTR_RBL_ADDR, tt->res_addr), + SEND_ATTR_INT(MAIL_ATTR_TTL, tt->res_ttl), + ATTR_TYPE_END); + mock_server_interact(mp, serialized_req, serialized_resp); + + /* + * Send a request by calling psc_dnsbl_request(), and run the + * event loop once to notify the mock dnsblog server that a + * request is pending. The mock dnsblog server will receive the + * request, and if it matches the expected request, the mock + * dnsblog server will immediately send the prepared response. + */ + session_state.req_addr = tt->req_addr; + session_state.got_dnsbl = 0; + session_state.got_ttl = INT_MAX; + session_state.got_score = INT_MAX; + session_state.req_idx = psc_dnsbl_request(tt->req_addr, + psc_dnsbl_callback, + &session_state); + event_loop(2); + + /* + * Run the event loop another time to wake up + * psc_dnsbl_receive(). That function will deserialize the mock + * dnsblog server's response, and will immediately call our + * psc_dnsbl_callback() function to store the result into the + * session_state object. + */ + event_loop(2); + + /* + * Validate the response. + */ + if (session_state.got_ttl == INT_MAX) { + ptest_error(t, "psc_dnsbl_callback() was not called, " + "or did not update the session_state"); + } else { + if (session_state.got_ttl != tt->res_ttl) + ptest_error(t, "unexpected ttl: got %d, want %d", + session_state.got_ttl, tt->res_ttl); + if (session_state.got_score != tt->want_score) + ptest_error(t, "unexpected score: got %d, want %d", + session_state.got_score, tt->want_score); + } + + /* + * Clean up. + */ + vstring_free(serialized_req); + vstring_free(serialized_resp); + mock_server_free(mp); + deinit_psc_globals(); + }); + } +} + + /* + * Test data and tests for multiple reputation providers. + */ +struct dnsbl_data { + const char *req_dnsbl; /* in dnsblog request */ + const char *res_addr; /* in dnsblog response */ + int res_ttl; /* in dnsblog response */ +}; + +#define MAX_DNSBL_SITES 3 + +struct multi_dnsbl_data { + const char *label; /* test label */ + const char *dnsbl_sites; /* postscreen_dnsbl_sites */ + const char *req_addr; /* in dnsblog request */ + struct dnsbl_data dnsbl_data[MAX_DNSBL_SITES]; + int want_ttl; /* effective TTL */ + int want_score; /* sum of weights */ +}; + +static const struct multi_dnsbl_data multi_dnsbl_tests[] = { + { + "dual dnsbl, listed by both", + /* dnsbl_sites */ "zen.spamhaus.org, foo.example.org", + /* req_addr */ "10.2.3.4", { + { + /* req_dnsbl */ "foo.example.org", + /* res_addr */ "127.0.0.10", + /* res_ttl */ 60, + }, + { + /* req_dnsbl */ "zen.spamhaus.org", + /* res_addr */ "127.0.0.10", + /* res_ttl */ 60, + }, + }, + /* want_ttl */ 60, + /* want_score */ 2, + }, { + "dual dnsbl, listed by first", + /* dnsbl_sites */ "zen.spamhaus.org, foo.example.org", + /* req_addr */ "10.2.3.4", { + { + /* req_dnsbl */ "foo.example.org", + /* res_addr */ "", + /* res_ttl */ 62, + }, + { + /* req_dnsbl */ "zen.spamhaus.org", + /* res_addr */ "127.0.0.10", + /* res_ttl */ 61, + }, + }, + /* want_ttl */ 61, + /* want_score */ 1, + }, { + "dual dnsbl, listed by last", + /* dnsbl_sites */ "zen.spamhaus.org, foo.example.org", + /* req_addr */ "10.2.3.4", { + { + /* req_dnsbl */ "foo.example.org", + /* res_addr */ "127.0.0.10", + /* res_ttl */ 62, + }, + { + /* req_dnsbl */ "zen.spamhaus.org", + /* res_addr */ "", + /* res_ttl */ 61, + }, + }, + /* want_ttl */ 62, + /* want_score */ 1, + }, { + "dual dnsbl, unlisted address zero score", + /* dnsbl_sites */ "zen.spamhaus.org, foo.example.org", + /* req_addr */ "10.2.3.4", { + { + /* req_dnsbl */ "foo.example.org", + /* res_addr */ "", + /* res_ttl */ 62, + }, + { + /* req_dnsbl */ "zen.spamhaus.org", + /* res_addr */ "", + /* res_ttl */ 61, + }, + }, + /* want_ttl */ 61, + /* want_score */ 0, + }, { + "dual dnsbl, allowlist wins", + /* dnsbl_sites */ "list.dnswl.org=127.0.[0..255].[1..3]*-2, foo.example.org", + /* req_addr */ "10.2.3.4", { + { + /* req_dnsbl */ "foo.example.org", + /* res_addr */ "127.0.0.10", + /* res_ttl */ 62, + }, + { + /* req_dnsbl */ "list.dnswl.org", + /* res_addr */ "127.0.5.2", + /* res_ttl */ 61, + }, + }, + /* want_ttl */ 61, + /* want_score */ -1, + } +}; + +static void test_multi_dnsbl(PTEST_CTX *t, const PTEST_CASE *tp) +{ + MOCK_SERVER *mp[MAX_DNSBL_SITES]; + struct session_state session_state; + const char *dnsblog_path = "private/dnsblog"; + const int request_id = 0; + const struct multi_dnsbl_data *tt; + const struct dnsbl_data *dp; + int n; + + for (tt = multi_dnsbl_tests; tt < multi_dnsbl_tests + + PTEST_NROF(multi_dnsbl_tests); tt++) { + if (tt->label == 0) + ptest_fatal(t, "Null test label in multi_dnsbl_tests array!"); + PTEST_RUN(t, tt->label, { + + /* + * Reset global state and parameters used by postscreen_dnsbl.c. + */ + init_psc_globals(tt->dnsbl_sites); + + for (n = 0, dp = tt->dnsbl_data; n < MAX_DNSBL_SITES + && dp[n].req_dnsbl != 0; n++) { + VSTRING *serialized_req; + VSTRING *serialized_resp; + + /* + * Instantiate a mock server. + */ + if ((mp[n] = mock_unix_server_create(dnsblog_path)) == 0) + ptest_fatal(t, "mock_unix_server_create: %m"); + + /* + * Set up the expected dnsblog requests, and the + * corresponding responses. The mock dnsblog server + * immediately generates read event requests, so we should + * send something soon. + */ + serialized_req = + make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, + dp[n].req_dnsbl), + SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, + tt->req_addr), + SEND_ATTR_INT(MAIL_ATTR_LABEL, request_id), + ATTR_TYPE_END); + serialized_resp = + make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, + dp[n].req_dnsbl), + SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, + tt->req_addr), + SEND_ATTR_INT(MAIL_ATTR_LABEL, request_id), + SEND_ATTR_STR(MAIL_ATTR_RBL_ADDR, + dp[n].res_addr), + SEND_ATTR_INT(MAIL_ATTR_TTL, + dp[n].res_ttl), + ATTR_TYPE_END); + mock_server_interact(mp[n], serialized_req, + serialized_resp); + vstring_free(serialized_req); + vstring_free(serialized_resp); + } + + /* + * Send a request by calling psc_dnsbl_request(), and run the + * event loop once to notify the mock dnsblog servers that a + * request is pending. Each mock dnsblog server will receive a + * request, and if it matches the expected request, the mock + * dnsblog server will immediately send the prepared response. + */ + session_state.req_addr = tt->req_addr; + session_state.got_dnsbl = 0; + session_state.got_ttl = INT_MAX; + session_state.got_score = INT_MAX; + session_state.req_idx = psc_dnsbl_request(tt->req_addr, + psc_dnsbl_callback, + &session_state); + event_loop(2); + + /* + * Run the event loop again, to wake up psc_dnsbl_receive(). That + * function will deserialize the mock dnsblog server's response, + * and will immediately call our psc_dnsbl_callback() function to + * store the result into the session_state object. + */ + event_loop(2); + + /* + * Validate the response. + */ + if (session_state.got_ttl == INT_MAX) { + ptest_error(t, "psc_dnsbl_callback() was not called, " + "or did not update the session_state"); + } else { + if (session_state.got_ttl != tt->want_ttl) + ptest_error(t, "unexpected ttl: got %d, want %d", + session_state.got_ttl, tt->want_ttl); + if (session_state.got_score != tt->want_score) + ptest_error(t, "unexpected score: got %d, want %d", + session_state.got_score, tt->want_score); + } + + /* + * Clean up. + */ + for (n = 0, dp = tt->dnsbl_data; n < MAX_DNSBL_SITES + && dp[n].req_dnsbl != 0; n++) + mock_server_free(mp[n]); + deinit_psc_globals(); + }); + } +} + + /* + * Test cases. + */ +const PTEST_CASE ptestcases[] = { + { + "single dnsbl", test_single_dnsbl, + }, + { + "multi dnsbl", test_multi_dnsbl, + }, +}; + +#include diff --git a/postfix/src/ptest/ptest.h b/postfix/src/ptest/ptest.h index ae7b57bc9..e2561016a 100644 --- a/postfix/src/ptest/ptest.h +++ b/postfix/src/ptest/ptest.h @@ -113,6 +113,11 @@ extern void ptest_defer(PTEST_CTX *, PTEST_DEFER_FN, void *); t = parent; \ } while (0) + /* + * How many elements in a test case array. + */ +#define PTEST_NROF(x) (sizeof(x)/sizeof((x)[0])) + /* LICENSE /* .ad /* .fi diff --git a/postfix/src/ptest/ptest_main.h b/postfix/src/ptest/ptest_main.h index ff2d5112a..b35907310 100644 --- a/postfix/src/ptest/ptest_main.h +++ b/postfix/src/ptest/ptest_main.h @@ -129,6 +129,14 @@ int main(int argc, char **argv) const PTEST_CASE *tp; int fail; + /* + * This must be set BEFORE the first hash table call. + */ +#ifndef DORANDOMIZE + if (putenv("NORANDOMIZE=") != 0) + msg_fatal("putenv() failed: %m"); +#endif + /* * Send msg(3) logging to stderr by default. */ @@ -147,9 +155,7 @@ int main(int argc, char **argv) * data, instead of having to store all test data in a PTEST_CASE * structure. */ -#define NROF(x) (sizeof(x)/sizeof((x)[0])) - - for (tp = ptestcases; tp < ptestcases + NROF(ptestcases); tp++) { + for (tp = ptestcases; tp < ptestcases + PTEST_NROF(ptestcases); tp++) { if (tp->testname == 0) msg_fatal("Null testname in ptestcases array!"); PTEST_RUN(t, tp->testname, { diff --git a/postfix/src/ptest/ptest_run.c b/postfix/src/ptest/ptest_run.c index 66c35cabc..52622bcd0 100644 --- a/postfix/src/ptest/ptest_run.c +++ b/postfix/src/ptest/ptest_run.c @@ -17,13 +17,13 @@ /* /* void ptest_defer( /* PTEST_CTX *t, -/* void (*defer_fn)(void *) +/* void (*defer_fn)(void *), /* void *defer_ctx) /* DESCRIPTION /* PTEST_RUN() is called from inside a test to run a subtest. /* /* PTEST_RUN() is a macro that runs the { body_in_braces } -/* with msg(3) logging temporarily redirected to a buffer, and +/* with msg(3) logging temporarily redirected to a listener, and /* with panic, fatal, error, and non-error functions that /* terminate a test without terminating the process. /* diff --git a/postfix/src/smtpd/Makefile.in b/postfix/src/smtpd/Makefile.in index f7d7bbb6a..61bb6cda4 100644 --- a/postfix/src/smtpd/Makefile.in +++ b/postfix/src/smtpd/Makefile.in @@ -342,6 +342,7 @@ smtpd_check.o: ../../include/mail_error.h smtpd_check.o: ../../include/mail_params.h smtpd_check.o: ../../include/mail_proto.h smtpd_check.o: ../../include/mail_stream.h +smtpd_check.o: ../../include/mail_version.h smtpd_check.o: ../../include/map_search.h smtpd_check.o: ../../include/maps.h smtpd_check.o: ../../include/match_list.h diff --git a/postfix/src/smtpd/smtpd_check.c b/postfix/src/smtpd/smtpd_check.c index 2785ce1fc..29e8671a4 100644 --- a/postfix/src/smtpd/smtpd_check.c +++ b/postfix/src/smtpd/smtpd_check.c @@ -253,6 +253,7 @@ #include #include #include +#include /* Application-specific. */ @@ -4101,6 +4102,8 @@ static int check_policy_service(SMTPD_STATE *state, const char *server, policy_clnt->policy_context), SEND_ATTR_STR(MAIL_ATTR_COMPAT_LEVEL, var_compatibility_level), + SEND_ATTR_STR(MAIL_ATTR_MAIL_VERSION, + var_mail_version), ATTR_TYPE_END, ATTR_FLAG_MISSING, /* Reply attributes. */ RECV_ATTR_STR(MAIL_ATTR_ACTION, action), diff --git a/postfix/src/testing/Makefile.in b/postfix/src/testing/Makefile.in index 6a6671200..09cde8bd2 100644 --- a/postfix/src/testing/Makefile.in +++ b/postfix/src/testing/Makefile.in @@ -1,20 +1,25 @@ SHELL = /bin/sh SRCS = mock_myaddrinfo.c mock_dns_lookup.c mock_servent.c \ mock_getaddrinfo.c match_basic.c match_addr.c make_addr.c \ - addrinfo_to_string.c -LIB_OBJ = match_basic.o match_addr.o make_addr.o addrinfo_to_string.o -MOCK_OBJ= mock_myaddrinfo.o mock_dns_lookup.o mock_servent.o mock_getaddrinfo.o + addrinfo_to_string.c make_attr.c match_attr.c +LIB_OBJ = match_basic.o match_addr.o make_addr.o addrinfo_to_string.o \ + make_attr.o match_attr.o +MOCK_OBJ= mock_myaddrinfo.o mock_dns_lookup.o mock_servent.o \ + mock_getaddrinfo.o mock_server.o TEST_OBJ = mock_dns_lookup_test.o mock_getaddrinfo_test.o \ - mock_myaddrinfo_test.o mock_servent_test.o match_addr_test.o + mock_myaddrinfo_test.o mock_servent_test.o match_addr_test.o \ + mock_server_test.o match_attr_test.o HDRS = mock_myaddrinfo.h mock_dns.h mock_servent.h mock_getaddrinfo.h \ - match_basic.h match_addr.h make_addr.h addrinfo_to_string.h + match_basic.h match_addr.h make_addr.h addrinfo_to_string.h \ + mock_server.h make_attr.h match_attr.h TESTSRC = DEFS = -I. -I$(INC_DIR) -D$(SYSTYPE) CFLAGS = $(DEBUG) $(OPT) $(DEFS) INCL = LIB = libtesting.a TESTPROG= mock_dns_lookup_test mock_getaddrinfo_test \ - mock_myaddrinfo_test mock_servent_test match_addr_test + mock_myaddrinfo_test mock_servent_test match_addr_test \ + match_attr_test mock_server_test LIBS = ../../lib/libptest.a \ ../../lib/lib$(LIB_PREFIX)dns$(LIB_SUFFIX) \ @@ -60,7 +65,7 @@ clean: tidy: clean tests: test_mock_dns_lookup test_mock_getaddrinfo test_mock_myaddrinfo \ - test_mock_servent test_match_addr + test_mock_servent test_match_addr test_mock_server mock_myaddrinfo_test: mock_myaddrinfo_test.o mock_myaddrinfo.o $(LIB) $(LIBS) $(CC) $(CFLAGS) -o $@ $@.o mock_myaddrinfo.o $(LIB) $(LIBS) $(SYSLIBS) @@ -92,6 +97,18 @@ match_addr_test: match_addr_test.o match_addr.o $(LIB) $(LIBS) test_match_addr: update match_addr_test $(SHLIB_ENV) ${VALGRIND} ./match_addr_test +mock_server_test: mock_server_test.o mock_server.o $(LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.o mock_server.o $(LIB) $(LIBS) $(SYSLIBS) + +test_mock_server: update mock_server_test + $(SHLIB_ENV) ${VALGRIND} ./mock_server_test + +match_attr_test: match_attr_test.o match_attr.o $(LIB) $(LIBS) + $(CC) $(CFLAGS) -o $@ $@.o match_attr.o $(LIB) $(LIBS) $(SYSLIBS) + +test_match_attr: update match_attr_test + $(SHLIB_ENV) ${VALGRIND} ./match_attr_test + depend: $(MAKES) (sed '1,/^# do not edit/!d' Makefile.in; \ set -e; for i in [a-z][a-z0-9]*.c; do \ @@ -119,6 +136,20 @@ make_addr.o: ../../include/sys_defs.h make_addr.o: ../../include/wrap_netdb.h make_addr.o: make_addr.c make_addr.o: make_addr.h +make_attr.o: ../../include/argv.h +make_attr.o: ../../include/attr.h +make_attr.o: ../../include/check_arg.h +make_attr.o: ../../include/htable.h +make_attr.o: ../../include/msg.h +make_attr.o: ../../include/mymalloc.h +make_attr.o: ../../include/nvtable.h +make_attr.o: ../../include/ptest.h +make_attr.o: ../../include/sys_defs.h +make_attr.o: ../../include/vbuf.h +make_attr.o: ../../include/vstream.h +make_attr.o: ../../include/vstring.h +make_attr.o: make_attr.c +make_attr.o: make_attr.h match_addr.o: ../../include/argv.h match_addr.o: ../../include/check_arg.h match_addr.o: ../../include/msg.h @@ -149,6 +180,40 @@ match_addr_test.o: ../../include/wrap_netdb.h match_addr_test.o: make_addr.h match_addr_test.o: match_addr.h match_addr_test.o: match_addr_test.c +match_attr.o: ../../include/argv.h +match_attr.o: ../../include/attr.h +match_attr.o: ../../include/check_arg.h +match_attr.o: ../../include/htable.h +match_attr.o: ../../include/msg.h +match_attr.o: ../../include/mymalloc.h +match_attr.o: ../../include/nvtable.h +match_attr.o: ../../include/ptest.h +match_attr.o: ../../include/sys_defs.h +match_attr.o: ../../include/vbuf.h +match_attr.o: ../../include/vstream.h +match_attr.o: ../../include/vstring.h +match_attr.o: match_attr.c +match_attr.o: match_attr.h +match_attr_test.o: ../../include/argv.h +match_attr_test.o: ../../include/attr.h +match_attr_test.o: ../../include/check_arg.h +match_attr_test.o: ../../include/htable.h +match_attr_test.o: ../../include/msg.h +match_attr_test.o: ../../include/msg_output.h +match_attr_test.o: ../../include/msg_vstream.h +match_attr_test.o: ../../include/mymalloc.h +match_attr_test.o: ../../include/nvtable.h +match_attr_test.o: ../../include/pmock_expect.h +match_attr_test.o: ../../include/ptest.h +match_attr_test.o: ../../include/ptest_main.h +match_attr_test.o: ../../include/stringops.h +match_attr_test.o: ../../include/sys_defs.h +match_attr_test.o: ../../include/vbuf.h +match_attr_test.o: ../../include/vstream.h +match_attr_test.o: ../../include/vstring.h +match_attr_test.o: make_attr.h +match_attr_test.o: match_attr.h +match_attr_test.o: match_attr_test.c match_basic.o: ../../include/argv.h match_basic.o: ../../include/check_arg.h match_basic.o: ../../include/msg.h @@ -304,3 +369,41 @@ mock_servent_test.o: ../../include/vstring.h mock_servent_test.o: ../../include/wrap_netdb.h mock_servent_test.o: mock_servent.h mock_servent_test.o: mock_servent_test.c +mock_server.o: ../../include/argv.h +mock_server.o: ../../include/check_arg.h +mock_server.o: ../../include/connect.h +mock_server.o: ../../include/events.h +mock_server.o: ../../include/iostuff.h +mock_server.o: ../../include/msg.h +mock_server.o: ../../include/mymalloc.h +mock_server.o: ../../include/ptest.h +mock_server.o: ../../include/sys_defs.h +mock_server.o: ../../include/vbuf.h +mock_server.o: ../../include/vstream.h +mock_server.o: ../../include/vstring.h +mock_server.o: match_attr.h +mock_server.o: mock_server.c +mock_server.o: mock_server.h +mock_server_test.o: ../../include/argv.h +mock_server_test.o: ../../include/attr.h +mock_server_test.o: ../../include/check_arg.h +mock_server_test.o: ../../include/connect.h +mock_server_test.o: ../../include/events.h +mock_server_test.o: ../../include/htable.h +mock_server_test.o: ../../include/iostuff.h +mock_server_test.o: ../../include/mail_proto.h +mock_server_test.o: ../../include/msg.h +mock_server_test.o: ../../include/msg_output.h +mock_server_test.o: ../../include/msg_vstream.h +mock_server_test.o: ../../include/mymalloc.h +mock_server_test.o: ../../include/nvtable.h +mock_server_test.o: ../../include/pmock_expect.h +mock_server_test.o: ../../include/ptest.h +mock_server_test.o: ../../include/ptest_main.h +mock_server_test.o: ../../include/stringops.h +mock_server_test.o: ../../include/sys_defs.h +mock_server_test.o: ../../include/vbuf.h +mock_server_test.o: ../../include/vstream.h +mock_server_test.o: ../../include/vstring.h +mock_server_test.o: mock_server.h +mock_server_test.o: mock_server_test.c diff --git a/postfix/src/testing/make_attr.c b/postfix/src/testing/make_attr.c new file mode 100644 index 000000000..1fd944b76 --- /dev/null +++ b/postfix/src/testing/make_attr.c @@ -0,0 +1,60 @@ +/*++ +/* NAME +/* make_attr 3 +/* SUMMARY +/* create serialized attribute request or response +/* SYNOPSIS +/* #include +/* +/* VSTRING *make_attr(int flags, ...) +/* DESCRIPTION +/* make_attr() creates a serialized request or response attribute +/* list. The arguments are like attr_print(). +/* 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 +/*--*/ + + /* + * System library. + */ +#include +#include + + /* + * Utility library. + */ +#include +#include + + /* + * Test library. + */ +#include +#include + +/* make_attr - serialize attribute list */ + +VSTRING *make_attr(int flags,...) +{ + static const char myname[] = "make_attr"; + VSTRING *res = vstring_alloc(100); + VSTREAM *stream; + va_list ap; + int err; + + if ((stream = vstream_memopen(res, O_WRONLY)) == 0) + ptest_fatal(ptest_ctx_current(), "%s: vstream_memopen: %m", myname);; + va_start(ap, flags); + err = attr_vprint(stream, flags, ap); + va_end(ap); + if (vstream_fclose(stream) != 0 || err) + ptest_fatal(ptest_ctx_current(), "%s: write attributes: %m", myname); + return (res); +} diff --git a/postfix/src/testing/make_attr.h b/postfix/src/testing/make_attr.h new file mode 100644 index 000000000..97659d901 --- /dev/null +++ b/postfix/src/testing/make_attr.h @@ -0,0 +1,35 @@ +#ifndef _MAKE_ATTR_H_INCLUDED_ +#define _MAKE_ATTR_H_INCLUDED_ + +/*++ +/* NAME +/* make_attr 3h +/* SUMMARY +/* create serialized attributes +/* SYNOPSIS +/* #include +/* DESCRIPTION +/* .nf + + /* + * Utility library. + */ +#include + + /* + * External interface. + */ +extern VSTRING *make_attr(int,...); + +/* 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 +/*--*/ + +#endif diff --git a/postfix/src/testing/match_addr_test.c b/postfix/src/testing/match_addr_test.c index c577468c6..12f8b5c47 100644 --- a/postfix/src/testing/match_addr_test.c +++ b/postfix/src/testing/match_addr_test.c @@ -25,6 +25,31 @@ typedef struct PTEST_CASE { void (*action) (PTEST_CTX *t, const struct PTEST_CASE *); } PTEST_CASE; +static void test_eq_addrinfo_equal(PTEST_CTX *t, const PTEST_CASE *unused) +{ + struct addrinfo hints; + struct addrinfo *want_addrinfo; + + /* + * Set up expectations. + */ + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_INET; + hints.ai_socktype = SOCK_STREAM; + want_addrinfo = make_addrinfo(&hints, "localhost", "127.0.0.1", 25); + + /* + * Verify that this addrinfo matches itself. + */ + if (!eq_addrinfo(t, "addrinfo", want_addrinfo, want_addrinfo)) + ptest_error(t, "eq_addrinfo() returned false for identical objects"); + + /* + * Clean up. + */ + freeaddrinfo(want_addrinfo); +} + static void test_eq_addrinfo_diff(PTEST_CTX *t, const PTEST_CASE *unused) { struct addrinfo hints; @@ -121,6 +146,9 @@ static void test_eq_sockaddr_diff(PTEST_CTX *t, const PTEST_CASE *unused) * Test cases. */ const PTEST_CASE ptestcases[] = { + { + "Compare equal IPv4 addrinfos", test_eq_addrinfo_equal, + }, { "Compare different IPv4 addrinfos", test_eq_addrinfo_diff, }, diff --git a/postfix/src/testing/match_attr.c b/postfix/src/testing/match_attr.c new file mode 100644 index 000000000..bd36760cd --- /dev/null +++ b/postfix/src/testing/match_attr.c @@ -0,0 +1,159 @@ +/*++ +/* NAME +/* match_attr 3 +/* SUMMARY +/* matchers for network address information +/* SYNOPSIS +/* #include +/* +/* int eq_attr( +/* PTEST_CTX *t, +/* const char *what, +/* VSTRING *got, +/* VSTRING *want) +/* DESCRIPTION +/* The functions described here are safe macros that include +/* call-site information (file name, line number) that may be +/* used in error messages. +/* +/* eq_attr() compares two serialized attribute lists and returns +/* whether their arguments contain the same values. If the t +/* argument is not null, eq_attr() will report details with +/* ptest_error()). +/* BUGS +/* An attribute name can appear only once in an attribute list. +/* 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 +/*--*/ + + /* + * System library. + */ +#include +#include +#include + + /* + * Utility library. + */ +#include +#include +#include + + /* + * Test library. + */ +#include +#include + +/* compar -qsort callback */ + +static int compar(const void *a, const void *b) +{ + return (strcmp((*(HTABLE_INFO **) a)->key, (*(HTABLE_INFO **) b)->key)); +} + +/* _eq_attr - match serialized attributes */ + +int _eq_attr(const char *file, int line, PTEST_CTX *t, + const char *what, VSTRING *got_buf, VSTRING *want_buf) +{ + static const char myname[] = "eq_attr"; + HTABLE *got_hash; + HTABLE *want_hash; + int count; + VSTREAM *mp; + HTABLE_INFO **ht_list; + HTABLE_INFO **ht; + char *ht_value; + + if (VSTRING_LEN(got_buf) == VSTRING_LEN(want_buf) + && memcmp(vstring_str(got_buf), vstring_str(want_buf), + VSTRING_LEN(got_buf)) == 0) + return (1); + + if (t != 0) { + + /* + * Deserialize the actual attributes into a hash. This loses order + * information. + */ + got_hash = htable_create(13); + if ((mp = vstream_memopen(got_buf, O_RDONLY)) == 0) + ptest_fatal(t, "%s: vstream_memopen: %m", myname); + count = attr_scan(mp, ATTR_FLAG_NONE, + ATTR_TYPE_HASH, got_hash, + ATTR_TYPE_END); + if (vstream_fclose(mp) != 0 || count <= 0) + ptest_fatal(t, "%s: vstream_fclose: %m", myname); + + /* + * Deserialize the wanted attributes into a hash. This loses order + * information. + */ + want_hash = htable_create(13); + if ((mp = vstream_memopen(want_buf, O_RDONLY)) == 0) + ptest_fatal(t, "%s: vstream_memopen: %m", myname); + count = attr_scan(mp, ATTR_FLAG_NONE, + ATTR_TYPE_HASH, want_hash, + ATTR_TYPE_END); + if (vstream_fclose(mp) != 0 || count <= 0) + ptest_fatal(t, "%s: vstream_fclose: %m", myname); + + /* + * Delete the intersection of the deserialized attribute lists. + */ + ht_list = htable_list(got_hash); + for (ht = ht_list; *ht; ht++) { + if ((ht_value = htable_find(want_hash, ht[0]->key)) != 0 + && strcmp(ht_value, ht[0]->value) == 0) { + htable_delete(want_hash, ht[0]->key, myfree); + /* At this point, ht_value is a dangling pointer. */ + htable_delete(got_hash, ht[0]->key, myfree); + /* At this point, ht is a dangling pointer. */ + } + } + myfree(ht_list); + + /* + * If the attributes differ only in order, then say so. We have no + * order information. This should never happen with real requests and + * responses. + */ + if (got_hash->used == 0 && want_hash->used == 0) { + ptest_error(t, "%s: attribute order differs", what); + } + + /* + * List differences in name or value. + */ + else { + ptest_error(t, "%s: attributes differ, +got/-want follows", what); + + /* + * Enumerate the unique attributes. + */ + ht_list = htable_list(got_hash); + qsort(ht_list, got_hash->used, sizeof(*ht_list), compar); + for (ht = ht_list; *ht; ht++) + ptest_error(t, "+%s = %s", ht[0]->key, (char *) ht[0]->value); + myfree(ht_list); + + ht_list = htable_list(want_hash); + qsort(ht_list, want_hash->used, sizeof(*ht_list), compar); + for (ht = ht_list; *ht; ht++) + ptest_error(t, "-%s = %s", ht[0]->key, (char *) ht[0]->value); + myfree(ht_list); + } + htable_free(got_hash, myfree); + htable_free(want_hash, myfree); + } + return (0); +} diff --git a/postfix/src/testing/match_attr.h b/postfix/src/testing/match_attr.h new file mode 100644 index 000000000..b91097dd0 --- /dev/null +++ b/postfix/src/testing/match_attr.h @@ -0,0 +1,44 @@ +#ifndef _MATCH_ATTR_H_INCLUDED_ +#define _MATCH_ATTR_H_INCLUDED_ + +/*++ +/* NAME +/* match_attr 3h +/* SUMMARY +/* attribute matching +/* SYNOPSIS +/* #include +/* DESCRIPTION +/* .nf + + /* + * Utility library. + */ +#include + + /* + * Test library. + */ +#include + + /* + * External interface. + */ +extern int _eq_attr(const char *, int, PTEST_CTX *, const char *, + VSTRING *, VSTRING *); + +#define eq_attr(t, what, got, want) \ + _eq_attr(__FILE__, __LINE__, (t), (what), (got), (want)) + +/* 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 +/*--*/ + +#endif diff --git a/postfix/src/testing/match_attr_test.c b/postfix/src/testing/match_attr_test.c new file mode 100644 index 000000000..c9fb5405b --- /dev/null +++ b/postfix/src/testing/match_attr_test.c @@ -0,0 +1,135 @@ +/* + * Test program to exercise match_attr functions including logging. See + * documentation in PTEST_README for the structure of this file. + */ + + /* + * System library. + */ +#include + + /* + * Utility library. + */ +#include +#include + + /* + * Test library. + */ +#include +#include +#include + +typedef struct PTEST_CASE { + const char *testname; /* Human-readable description */ + void (*action) (PTEST_CTX *t, const struct PTEST_CASE *); +} PTEST_CASE; + +static void test_eq_attr_equal(PTEST_CTX *t, const PTEST_CASE *unused) +{ + VSTRING *want_attr; + + /* + * Serialize some attributes. + */ + want_attr = make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR("this-key", "this-value"), + SEND_ATTR_STR("that-key", "that-value"), + ATTR_TYPE_END); + + /* + * VERIFY that this serialized attribute list matches ifself. + */ + if (!eq_attr(t, "want_attr", want_attr, want_attr)) + ptest_fatal(t, "eq_attr() returned false for identical objects"); + + /* + * Clean up. + */ + vstring_free(want_attr); +} + +static void test_eq_attr_swapped(PTEST_CTX *t, const PTEST_CASE *unused) +{ + VSTRING *want_attr; + VSTRING *swapped_attr; + + /* + * Serialize some attributes. + */ + want_attr = make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR("this-key", "this-value"), + SEND_ATTR_STR("that-key", "that-value"), + ATTR_TYPE_END); + swapped_attr = make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR("that-key", "that-value"), + SEND_ATTR_STR("this-key", "this-value"), + ATTR_TYPE_END); + + /* + * VERIFY that eq_attr() report attributes that differ only in order. + */ + expect_ptest_error(t, "attribute order differs"); + if (eq_attr(t, "want_attr", swapped_attr, want_attr)) + ptest_fatal(t, "eq_attr() returned true for swapped objects"); + + /* + * Clean up. + */ + vstring_free(want_attr); +} + +static void test_eq_attr_diff(PTEST_CTX *t, const PTEST_CASE *unused) +{ + VSTRING *want_attr; + VSTRING *swapped_attr; + + /* + * Serialize some attributes. + */ + want_attr = make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR("this-key", "this-value"), + SEND_ATTR_STR("that-key", "that-value"), + SEND_ATTR_STR("same-key", "same-value"), + ATTR_TYPE_END); + swapped_attr = make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR("not-this-key", "this-value"), + SEND_ATTR_STR("that-key", "not-that-value"), + SEND_ATTR_STR("same-key", "same-value"), + ATTR_TYPE_END); + + /* + * Verify that match_attr reports the expected differences. + */ + expect_ptest_error(t, "attributes differ"); + expect_ptest_error(t, "+not-this-key = this-value"); + expect_ptest_error(t, "+that-key = not-that-value"); + expect_ptest_error(t, "-that-key = that-value"); + expect_ptest_error(t, "-this-key = this-value"); + if (eq_attr(t, "want_attr", swapped_attr, want_attr)) + ptest_fatal(t, "eq_attr() returned true for different objects"); + + /* + * Clean up. + */ + vstring_free(want_attr); + vstring_free(swapped_attr); +} + + /* + * Test cases. + */ +const PTEST_CASE ptestcases[] = { + { + "Compare identical attribute lists", test_eq_attr_equal, + }, + { + "Compare swapped attribute lists", test_eq_attr_swapped, + }, + { + "Compare different attribute lists", test_eq_attr_diff, + }, +}; + +#include diff --git a/postfix/src/testing/mock_server.c b/postfix/src/testing/mock_server.c new file mode 100644 index 000000000..35db1d935 --- /dev/null +++ b/postfix/src/testing/mock_server.c @@ -0,0 +1,331 @@ +/*++ +/* NAME +/* mock_server 3 +/* SUMMARY +/* Mock server for hermetic tests +/* SYNOPSIS +/* #include +/* +/* MOCK_SERVER *mock_unix_server_create( +/* const char *destination) +/* +/* void mock_server_interact( +/* MOCK_SERVER *server, +/* const VSTRING *request, +/* const VSTRING *response) +/* +/* void mock_server_free(MOCK_SERVER *server) +/* +/* int unix_connect( +/* const char *destination, +/* int block_mode, +/* int timeout) +/* AUXILIARY FUNCTIONS +/* void mock_server_free_void_ptr(void *ptr) +/* DESCRIPTION +/* The purpose of this code is to make tests hermetic, i.e. +/* independent from a real server. +/* +/* This module overrides the client function unix_connect() +/* with a function that connects to a mock server instance. +/* The mock server must be instantiated in advance with +/* mock_unix_server_create(). The connection destination name +/* is not associated with out-of-process resources. +/* +/* mock_unix_server_create() creates a mock in-process server +/* that will "accept" one unix_connect() request with the +/* specified destination. To accept multiple connections, use +/* multiple mock_unix_server_create() calls. +/* +/* mock_server_interact() sets up one expected request and/or +/* prepared response. Specify a null request to configure an +/* unconditional server response such as an initial handshake, +/* and specify a null response to specify a final request. If +/* an expected request is configured, the client should send +/* a request and call event_loop() once to deliver the request +/* to the mock server. If a prepared response is configured, +/* the mock server will respond immediately, and the client +/* should call event_loop() once to receive the response from +/* the server. To simulate multiple request/response interactions, +/* use a sequence of mock_server_interact() calls. +/* +/* mock_server_free() destroys the specified server instance. +/* mock_server_free_void_ptr() provides an alternative API to +/* allow for systems where (struct *) and (void *) differ. +/* BUGS +/* Each connection supports no more than one request and one +/* response at a time; each request and each response must fit +/* in a VSTREAM buffer (4096 bytes as of Postfix version +/* 3.8), and must not be larger than SO_SNDBUF for AF_LOCAL +/* stream sockets (8192 bytes or more on contemporaneous +/* systems). +/* 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 +/*--*/ + + /* + * System library. + */ +#include +#include +#include +#include + + /* + * Utility library. + */ +#include +#include +#include +#include + + /* + * Test libraries + */ +#include +#include +#include + + /* + * Macros to make obscure code more readable. + */ +#define COPY_OR_NULL(dst, src) do { \ + if ((src) != 0) { \ + if ((dst) == 0) \ + (dst) = vstring_alloc(LEN(src)); \ + vstring_memcpy((dst), STR(src), LEN(src)); \ + } else if ((dst) != 0) { \ + vstring_free(dst); \ + (dst) = 0; \ + } \ + } while (0) + +#define MOCK_SERVER_REQUEST_READ_EVENT(fd, action, context, timeout) do { \ + if (msg_verbose > 1) \ + msg_info("%s: read-request fd=%d", myname, (fd)); \ + event_enable_read((fd), (action), (context)); \ + event_request_timer((action), (context), (timeout)); \ + } while (0) + +#define MOCK_SERVER_CLEAR_EVENT_REQUEST(fd, time_act, context) do { \ + if (msg_verbose > 1) \ + msg_info("%s: clear-request fd=%d", myname, (fd)); \ + event_disable_readwrite(fd); \ + event_cancel_timer((time_act), (context)); \ + } while (0) + + +#define MOCK_SERVER_TIMEOUT 10 + +#define MOCK_SERVER_SIDE (0) +#define MOCK_CLIENT_SIDE (1) + + /* + * Other macros. + */ +#define STR vstring_str +#define LEN VSTRING_LEN + + /* + * List head. We could use a RING object and save a few bits in data space, + * at the cost of more complex code. + */ +static MOCK_SERVER mock_unix_server_head; + +/* mock_unix_server_create - instantiate an unconnected mock server */ + +MOCK_SERVER *mock_unix_server_create(const char *dest) +{ + MOCK_SERVER *mp; + + mp = (MOCK_SERVER *) mymalloc(sizeof(*mp)); + if (socketpair(AF_LOCAL, SOCK_STREAM, 0, mp->fds) < 0) { + myfree(mp); + ptest_error(ptest_ctx_current(), "mock_unix_server_create(%s): " + "socketpair(AF_LOCAL, SOCK_STREAM, 0, fds): %m", dest); + return (0); + } + mp->flags = 0; + mp->want_dest = mystrdup(dest); + mp->want_req = 0; + mp->resp = 0; + mp->iobuf = 0; + + /* + * Link the mock server into the waiting list. + */ + mp->next = mock_unix_server_head.next; + mock_unix_server_head.next = mp; + mp->prev = &mock_unix_server_head; + if (mp->next) + mp->next->prev = mp; + return (mp); +} + +/* mock_unix_server_respond - send a prepared response */ + +static void mock_unix_server_respond(MOCK_SERVER *mp) +{ + const char myname[] = "mock_unix_server_respond"; + ssize_t got_len; + + got_len = write(mp->fds[MOCK_SERVER_SIDE], STR(mp->resp), LEN(mp->resp)); + if (got_len < 0) + ptest_fatal(ptest_ctx_current(), "%s: write: %m", myname); + if (got_len != LEN(mp->resp)) + ptest_fatal(ptest_ctx_current(), "%s: wrote %ld of %ld bytes", + myname, (long) got_len, (long) LEN(mp->resp)); +} + +/* mock_unix_server_read_event - receive request and respond */ + +static void mock_unix_server_read_event(int event, void *context) +{ + const char myname[] = "mock_unix_server_read_event"; + MOCK_SERVER *mp = (MOCK_SERVER *) context; + ssize_t peek_len; + ssize_t got_len; + + /* + * Disarm this file descriptor. + */ + MOCK_SERVER_CLEAR_EVENT_REQUEST(mp->fds[MOCK_SERVER_SIDE], + mock_unix_server_read_event, + context); + + /* + * Handle the event. + */ + switch (event) { + case EVENT_READ: + break; + case EVENT_TIME: + ptest_error(ptest_ctx_current(), "%s: timeout", myname); + return; + default: + ptest_fatal(ptest_ctx_current(), "%s: unexpected event: %d", + myname, event); + } + + /* + * Receive the request. + */ + switch (peek_len = peekfd(mp->fds[MOCK_SERVER_SIDE])) { + case -1: + ptest_error(ptest_ctx_current(), "%s: read: %m", myname); + return; + case 0: + ptest_error(ptest_ctx_current(), "%s: read EOF", myname); + return; + default: + break; + } + if (mp->iobuf == 0) + mp->iobuf = vstring_alloc(1000); + VSTRING_SPACE(mp->iobuf, peek_len); + got_len = read(mp->fds[MOCK_SERVER_SIDE], STR(mp->iobuf), peek_len); + if (got_len != peek_len) { + ptest_fatal(ptest_ctx_current(), "%s: read %ld of %ld bytes", + myname, (long) got_len, (long) peek_len); + return; + } + vstring_set_payload_size(mp->iobuf, got_len); + if (!eq_attr(ptest_ctx_current(), "request", mp->iobuf, mp->want_req)) + return; + + /* + * Send the response, if available. + */ + else if (mp->resp) + mock_unix_server_respond(mp); +} + +/* mock_server_interact - set up request and/or response */ + +void mock_server_interact(MOCK_SERVER *mp, + const VSTRING *req, + const VSTRING *resp) +{ + const char myname[] = "mock_server_interact"; + + if (req == 0 && resp == 0) + ptest_fatal(ptest_ctx_current(), "%s: null request and null response", + myname); + COPY_OR_NULL(mp->want_req, req); + COPY_OR_NULL(mp->resp, resp); + if (req != 0) { + MOCK_SERVER_REQUEST_READ_EVENT(mp->fds[MOCK_SERVER_SIDE], + mock_unix_server_read_event, + (void *) mp, MOCK_SERVER_TIMEOUT); + } else { + mock_unix_server_respond(mp); + } +} + +/* mock_unix_server_unlink - detach one instance from the waiting list */ + +static void mock_unix_server_unlink(MOCK_SERVER *mp) +{ + if (mp->next) + mp->next->prev = mp->prev; + if (mp->prev) + mp->prev->next = mp->next; + mp->prev = mp->next = 0; +} + +/* mock_server_free - destroy mock unix-domain server */ + +void mock_server_free(MOCK_SERVER *mp) +{ + const char myname[] = "mock_server_free"; + + myfree(mp->want_dest); + if ((mp->flags & MOCK_SERVER_FLAG_CONNECTED) == 0) + (void) close(mp->fds[MOCK_CLIENT_SIDE]); + MOCK_SERVER_CLEAR_EVENT_REQUEST(mp->fds[MOCK_SERVER_SIDE], + mock_unix_server_read_event, mp); + (void) close(mp->fds[MOCK_SERVER_SIDE]); + if (mp->want_req) + vstring_free(mp->want_req); + if (mp->resp) + vstring_free(mp->resp); + if (mp->iobuf) + vstring_free(mp->iobuf); + mock_unix_server_unlink(mp); + myfree(mp); +} + +/* mock_server_free_void_ptr - destroy mock unix-domain server */ + +void mock_server_free_void_ptr(void *ptr) +{ + mock_server_free(ptr); +} + +/* unix_connect - mock helper */ + +int unix_connect(const char *dest, int block_mode, int unused_timeout) +{ + MOCK_SERVER *mp; + + for (mp = mock_unix_server_head.next; /* see below */ ; mp = mp->next) { + if (mp == 0) { + errno = ENOENT; + return (-1); + } + if (strcmp(dest, mp->want_dest) == 0) { + mock_unix_server_unlink(mp); + if (block_mode == NON_BLOCKING) + non_blocking(mp->fds[MOCK_CLIENT_SIDE], block_mode); + mp->flags |= MOCK_SERVER_FLAG_CONNECTED; + return (mp->fds[MOCK_CLIENT_SIDE]); + } + } +} diff --git a/postfix/src/testing/mock_server.h b/postfix/src/testing/mock_server.h new file mode 100644 index 000000000..c97d63449 --- /dev/null +++ b/postfix/src/testing/mock_server.h @@ -0,0 +1,53 @@ +#ifndef _MOCK_SERVER_H_INCLUDED_ +#define _MOCK_SERVER_H_INCLUDED_ + +/*++ +/* NAME +/* mock_server 3h +/* SUMMARY +/* Mock server support +/* SYNOPSIS +/* #include +/* DESCRIPTION +/* .nf + + /* + * Utility library. + */ +#include +#include + + /* + * External interface. + */ +typedef struct MOCK_SERVER { + int flags; + int fds[2]; /* pipe(2) result */ + char *want_dest; + VSTRING *want_req; /* serialized request, may be null */ + VSTRING *resp; /* serialized response, may be null */ + VSTRING *iobuf; /* I/O buffer */ + struct MOCK_SERVER *next; /* chain of unconnected servers */ + struct MOCK_SERVER *prev; /* chain of unconnected servers */ +} MOCK_SERVER; + +#define MOCK_SERVER_FLAG_CONNECTED (1<<0) + +extern MOCK_SERVER *mock_unix_server_create(const char *); +extern void mock_server_interact(MOCK_SERVER *, const VSTRING *, + const VSTRING *); +extern void mock_server_free(MOCK_SERVER *); +extern void mock_server_free_void_ptr(void *); + +/* 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 +/*--*/ + +#endif diff --git a/postfix/src/testing/mock_server_test.c b/postfix/src/testing/mock_server_test.c new file mode 100644 index 000000000..4dd8ee9e9 --- /dev/null +++ b/postfix/src/testing/mock_server_test.c @@ -0,0 +1,426 @@ + /* + * Test program to exercise mock_server.c. See PTEST_README for + * documentation for how this file is structured. + */ + + /* + * System library. + */ +#include +#include +#include + + /* + * Utility library. + */ +#include +#include +#include +#include +#include + + /* + * Global library. + */ +#include + + /* + * Test library. + */ +#include +#include +#include + + /* + * Generic case structure. + */ +typedef struct PTEST_CASE { + const char *testname; /* Human-readable description */ + void (*action) (PTEST_CTX *t, const struct PTEST_CASE *); +} PTEST_CASE; + + /* + * Structure to capture a client-server conversation state. + */ +struct session_state { + VSTRING *resp_buf; /* request echoed by server */ + int resp_len; /* request length from server */ + int fd; + VSTREAM *stream; + int error; +}; + +#define REQUEST_READ_EVENT(fd, action, context, timeout) do { \ + if (msg_verbose > 1) \ + msg_info("%s: read-request fd=%d", myname, (fd)); \ + event_enable_read((fd), (action), (context)); \ + event_request_timer((action), (context), (timeout)); \ + } while (0) + +#define CLEAR_EVENT_REQUEST(fd, time_act, context) do { \ + if (msg_verbose > 1) \ + msg_info("%s: clear-request fd=%d", myname, (fd)); \ + event_disable_readwrite(fd); \ + event_cancel_timer((time_act), (context)); \ + } while (0) + +/* read_event - event handler to receive server response */ + +static void read_event(int event, void *context) +{ + static const char myname[] = "read_event"; + struct session_state *session_state = (struct session_state *) context; + + CLEAR_EVENT_REQUEST(session_state->fd, read_event, context); + + switch (event) { + case EVENT_READ: + if (attr_scan(session_state->stream, ATTR_FLAG_NONE, + RECV_ATTR_STR(MAIL_ATTR_REQ, session_state->resp_buf), + RECV_ATTR_INT(MAIL_ATTR_SIZE, &session_state->resp_len), + ATTR_TYPE_END) != 2) { + ptest_error(ptest_ctx_current(), "%s failed: %m", myname); + session_state->error = EINVAL; + } + break; + case EVENT_TIME: + ptest_error(ptest_ctx_current(), "%s: timeout", myname); + session_state->error = ETIMEDOUT; + break; + default: + ptest_fatal(ptest_ctx_current(), "%s: unknown event: %d", + myname, event); + } +} + +static void test_single_server(PTEST_CTX *t, const PTEST_CASE *tp) +{ + static const char myname[] = "test_single_server"; + MOCK_SERVER *mp; + struct session_state session_state; + VSTRING *serialized_req; + VSTRING *serialized_resp; + +#define REQUEST_VAL "abcdef" +#define SERVER_NAME "testing..." + + /* + * Instantiate a mock server, and connect to it. + */ + mp = mock_unix_server_create(SERVER_NAME); + session_state.resp_buf = vstring_alloc(100); + session_state.resp_len = 0; + session_state.error = 0; + if ((session_state.fd = unix_connect(SERVER_NAME, 0, 0)) < 0) + ptest_fatal(t, "unix_connect: %s: %m", SERVER_NAME); + session_state.stream = vstream_fdopen(session_state.fd, O_RDWR); + + /* + * Set up a server request expectation, and response. + */ + serialized_req = + make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL), + ATTR_TYPE_END); + serialized_resp = + make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL), + SEND_ATTR_INT(MAIL_ATTR_SIZE, strlen(REQUEST_VAL)), + ATTR_TYPE_END); + mock_server_interact(mp, serialized_req, serialized_resp); + + /* + * Send a request, and run the event loop once to notify the server side + * that the request is pending. + */ + if (attr_print(session_state.stream, ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL), + ATTR_TYPE_END) != 0 + || vstream_fflush(session_state.stream) != 0) + ptest_fatal(t, "send request: %m"); + event_loop(1); + + /* + * Receive the response, and validate. + */ + REQUEST_READ_EVENT(session_state.fd, read_event, &session_state, 1); + event_loop(1); + if (session_state.error != 0) { + /* already reported */ + } else if (VSTRING_LEN(session_state.resp_buf) != strlen(REQUEST_VAL)) { + ptest_error(t, "got resp_buf length %ld, want %ld", + (long) VSTRING_LEN(session_state.resp_buf), + (long) strlen(REQUEST_VAL)); + } else if (session_state.resp_len != strlen(REQUEST_VAL)) { + ptest_error(t, "got resp_len %d, want %ld", + session_state.resp_len, (long) strlen(REQUEST_VAL)); + } else if (strcmp(vstring_str(session_state.resp_buf), REQUEST_VAL) != 0) { + ptest_error(t, "got resp_buf '%s', wamt '%s'", + vstring_str(session_state.resp_buf), REQUEST_VAL); + } + + /* + * Clean up. + */ + if (vstream_fclose(session_state.stream) != 0) + ptest_fatal(t, "close stream: %m"); + vstring_free(session_state.resp_buf); + vstring_free(serialized_req); + vstring_free(serialized_resp); + mock_server_free(mp); +} + +static void test_request_mismatch(PTEST_CTX *t, const PTEST_CASE *tp) +{ + static const char myname[] = "test_single_server"; + MOCK_SERVER *mp; + struct session_state session_state; + VSTRING *serialized_req; + VSTRING *serialized_resp; + +#define REQUEST_VAL "abcdef" +#define SERVER_NAME "testing..." + + /* + * Instantiate a mock server, and connect to it. + */ + mp = mock_unix_server_create(SERVER_NAME); + session_state.resp_buf = vstring_alloc(100); + session_state.resp_len = 0; + if ((session_state.fd = unix_connect(SERVER_NAME, 0, 0)) < 0) + ptest_fatal(t, "unix_connect: %s: %m", SERVER_NAME); + session_state.stream = vstream_fdopen(session_state.fd, O_RDWR); + + /* + * Set up a server request expectation, and response. + */ + serialized_req = + make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL "g"), + ATTR_TYPE_END); + serialized_resp = + make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL), + SEND_ATTR_INT(MAIL_ATTR_SIZE, strlen(REQUEST_VAL)), + ATTR_TYPE_END); + mock_server_interact(mp, serialized_req, serialized_resp); + + /* + * Send a request, and run the event loop once to notify the server side + * that the request is pending. + */ + if (attr_print(session_state.stream, ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL), + ATTR_TYPE_END) != 0 + || vstream_fflush(session_state.stream) != 0) + ptest_fatal(t, "send request: %m"); + expect_ptest_error(t, "attributes differ"); + expect_ptest_error(t, "+request = abcdef"); + expect_ptest_error(t, "-request = abcdefg"); + expect_ptest_error(t, "timeout"); + event_loop(1); + + /* + * Receive the response, and validate. + */ + REQUEST_READ_EVENT(session_state.fd, read_event, &session_state, 1); + event_loop(1); + if (session_state.error != 0) { + /* already reported */ + } else if (VSTRING_LEN(session_state.resp_buf) != strlen(REQUEST_VAL)) { + ptest_error(t, "got resp_buf length %ld, want %ld", + (long) VSTRING_LEN(session_state.resp_buf), + (long) strlen(REQUEST_VAL)); + } else if (session_state.resp_len != strlen(REQUEST_VAL)) { + ptest_error(t, "got resp_len %d, want %ld", + session_state.resp_len, (long) strlen(REQUEST_VAL)); + } else if (strcmp(vstring_str(session_state.resp_buf), REQUEST_VAL) != 0) { + ptest_error(t, "got resp_buf '%s', wamt '%s'", + vstring_str(session_state.resp_buf), REQUEST_VAL); + } + + /* + * Clean up. + */ + if (vstream_fclose(session_state.stream) != 0) + ptest_fatal(t, "close stream: %m"); + vstring_free(session_state.resp_buf); + vstring_free(serialized_req); + vstring_free(serialized_resp); + mock_server_free(mp); +} + +static void test_missing_server(PTEST_CTX *t, const PTEST_CASE *tp) +{ + int fd; + +#define SERVER_NAME "testing..." + + /* + * Connect to a non-existent server, and require a failure. + */ + if ((fd = unix_connect(SERVER_NAME, 0, 0)) >= 0) { + (void) close(fd); + ptest_fatal(t, + "unix_connect(%s) did NOT fail", SERVER_NAME); + } +} + +static void test_unused_server(PTEST_CTX *t, const PTEST_CASE *tp) +{ + MOCK_SERVER *mp; + + /* + * Instantiate a mock server, and destroy it without using it. + */ + mp = mock_unix_server_create(SERVER_NAME); + mock_server_free(mp); +} + +static void test_server_speaks_only(PTEST_CTX *t, const PTEST_CASE *tp) +{ + static const char myname[] = "test_server_speaks_only"; + MOCK_SERVER *mp; + struct session_state session_state; + VSTRING *serialized_resp; + + /* + * This is the same test as "test_single_server", but without sending a + * request. + */ +#define REQUEST_VAL "abcdef" +#define SERVER_NAME "testing..." +#define NO_REQUEST ((VSTRING *) 0) + + /* + * Instantiate a mock server, and connect to it. + */ + mp = mock_unix_server_create(SERVER_NAME); + session_state.resp_buf = vstring_alloc(100); + session_state.resp_len = 0; + if ((session_state.fd = unix_connect(SERVER_NAME, 0, 0)) < 0) + ptest_fatal(t, "unix_connect: %s: %m", SERVER_NAME); + session_state.stream = vstream_fdopen(session_state.fd, O_RDWR); + + /* + * Set up a server response, without request expectation. + */ + serialized_resp = + make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL), + SEND_ATTR_INT(MAIL_ATTR_SIZE, strlen(REQUEST_VAL)), + ATTR_TYPE_END); + mock_server_interact(mp, NO_REQUEST, serialized_resp); + + /* + * Receive the response, and validate. + */ + REQUEST_READ_EVENT(session_state.fd, read_event, &session_state, 1); + event_loop(1); + if (session_state.error != 0) { + /* already reported */ + } else if (VSTRING_LEN(session_state.resp_buf) != strlen(REQUEST_VAL)) { + ptest_error(t, "got resp_buf length %ld, want %ld", + (long) VSTRING_LEN(session_state.resp_buf), + (long) strlen(REQUEST_VAL)); + } else if (session_state.resp_len != strlen(REQUEST_VAL)) { + ptest_error(t, "got resp_len %d, want %ld", + session_state.resp_len, (long) strlen(REQUEST_VAL)); + } else if (strcmp(vstring_str(session_state.resp_buf), REQUEST_VAL) != 0) { + ptest_error(t, "got resp_buf '%s', wamt '%s'", + vstring_str(session_state.resp_buf), REQUEST_VAL); + } + + /* + * Clean up. + */ + if (vstream_fclose(session_state.stream) != 0) + ptest_fatal(t, "close stream: %m"); + vstring_free(session_state.resp_buf); + vstring_free(serialized_resp); + mock_server_free(mp); +} + +static void test_client_speaks_only(PTEST_CTX *t, const PTEST_CASE *tp) +{ + MOCK_SERVER *mp; + struct session_state session_state; + VSTRING *serialized_req; + + /* + * This is the same test as "test_single_server", but without receiving a + * response. + */ +#define REQUEST_VAL "abcdef" +#define SERVER_NAME "testing..." +#define NO_RESPONSE ((VSTRING *) 0) + + /* + * Instantiate a mock server, and connect to it. + */ + mp = mock_unix_server_create(SERVER_NAME); + if ((session_state.fd = unix_connect(SERVER_NAME, 0, 0)) < 0) + ptest_fatal(t, "unix_connect: %s: %m", SERVER_NAME); + session_state.stream = vstream_fdopen(session_state.fd, O_RDWR); + + /* + * Set up a server request expectation, and response. + */ + serialized_req = + make_attr(ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL), + ATTR_TYPE_END); + mock_server_interact(mp, serialized_req, NO_RESPONSE); + + /* + * Send a request, and run the event loop once to notify the server side + * that the request is pending. + */ + if (attr_print(session_state.stream, ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL), + ATTR_TYPE_END) != 0 + || vstream_fflush(session_state.stream) != 0) + ptest_fatal(t, "send request: %m"); + event_loop(1); + + /* + * Clean up. + */ + if (vstream_fclose(session_state.stream) != 0) + ptest_fatal(t, "close stream: %m"); + vstring_free(serialized_req); + mock_server_free(mp); +} + + /* + * Test cases. + */ +const PTEST_CASE ptestcases[] = { + { + "test single server", test_single_server, + }, + { + "test request mismatch", test_request_mismatch, + }, + { + "test missing server", test_missing_server, + }, + { + "test unused server", test_unused_server, + }, + { + "test server speaks only", test_server_speaks_only, + }, + { + "test client speaks only", test_client_speaks_only, + }, + + /* + * TODO: test multiple servers with the same endpoint name but with + * different expectations. See postscreen_dnsbl_test.c for an example. + * This requires that the environment variable "NORAMDOMIZE" is set + * before this program is run. + */ +}; + +#include diff --git a/postfix/src/util/argv_test.c b/postfix/src/util/argv_test.c index d604962f3..95218ca60 100644 --- a/postfix/src/util/argv_test.c +++ b/postfix/src/util/argv_test.c @@ -1,6 +1,5 @@ /* - * Test program to exercise argv.c. See ptest_main.h for a documented - * example. + * Test program to exercise argv.c. See PTEST_README for documentation. */ /* diff --git a/postfix/src/util/dict_pipe.c b/postfix/src/util/dict_pipe.c index 8ce0faad7..146e837a2 100644 --- a/postfix/src/util/dict_pipe.c +++ b/postfix/src/util/dict_pipe.c @@ -37,6 +37,11 @@ /* IBM T.J. Watson Research /* P.O. Box 704 /* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA /*--*/ /* System library. */ diff --git a/postfix/src/util/dict_pipe_test.c b/postfix/src/util/dict_pipe_test.c index 4a566ef33..d56dffd2c 100644 --- a/postfix/src/util/dict_pipe_test.c +++ b/postfix/src/util/dict_pipe_test.c @@ -1,6 +1,5 @@ /* - * Test program to exercise dict_pipe.c. See ptest_main.h for a documented - * example. + * Test program to exercise dict_pipe.c. See PTEST_README for documentation. */ /* diff --git a/postfix/src/util/dict_stream_test.c b/postfix/src/util/dict_stream_test.c index 47332b422..5cbb54870 100644 --- a/postfix/src/util/dict_stream_test.c +++ b/postfix/src/util/dict_stream_test.c @@ -1,6 +1,6 @@ /* - * Test program to exercise dict_stream.c. See ptest_main.h for a documented - * example. + * Test program to exercise dict_stream.c. See PTEST_README for + * documentation. */ /* diff --git a/postfix/src/util/dict_union_test.c b/postfix/src/util/dict_union_test.c index 845dc9b31..83b4e8ae9 100644 --- a/postfix/src/util/dict_union_test.c +++ b/postfix/src/util/dict_union_test.c @@ -1,6 +1,6 @@ /* - * Test program to exercise dict_union.c. See ptest_main.h for a documented - * example. + * Test program to exercise dict_union.c. See PTEST_README for + * documentation. */ /* @@ -77,7 +77,7 @@ static void test_dict_union(PTEST_CTX *t, const struct PTEST_CASE *tp) static const PTEST_CASE ptestcases[] = { { - /* name */ "successful lookup: static map + inline map", + /* testname */ "successful lookup: static map + inline map", /* action */ test_dict_union, /* type_name */ "unionmap:{static:one,inline:{foo=two}}", /* probes */ { @@ -85,14 +85,14 @@ static const PTEST_CASE ptestcases[] = { {"bar", "one"}, }, }, { - /* name */ "error propagation: static map + fail map", + /* testname */ "error propagation: static map + fail map", /* action */ test_dict_union, /* type_name */ "unionmap:{static:one,fail:fail}", /* probes */ { {"foo", 0, DICT_STAT_ERROR}, }, }, { - /* name */ "error propagation: fail map + static map", + /* testname */ "error propagation: fail map + static map", /* action */ test_dict_union, /* type_name */ "unionmap:{fail:fail,static:one}", /* probes */ { diff --git a/postfix/src/util/find_inet_service_test.c b/postfix/src/util/find_inet_service_test.c index 11b60b39c..ba806e80c 100644 --- a/postfix/src/util/find_inet_service_test.c +++ b/postfix/src/util/find_inet_service_test.c @@ -1,6 +1,6 @@ /* * Test program to exercise find_inet_service.c. See pmock_expect_test.c and - * ptest_main.h for a documented example. + * PTEST_README for documentation. */ /* diff --git a/postfix/src/util/hash_fnv_test.c b/postfix/src/util/hash_fnv_test.c index 6255a1d81..a2f85eddb 100644 --- a/postfix/src/util/hash_fnv_test.c +++ b/postfix/src/util/hash_fnv_test.c @@ -1,6 +1,6 @@ /* - * Test program to exercise the hash_fnv implementation. See comments in - * ptest_main.h for a documented example. + * Test program to exercise the hash_fnv implementation. See PTEST_README + * for documentation. */ /* diff --git a/postfix/src/util/known_tcp_ports_test.c b/postfix/src/util/known_tcp_ports_test.c index bdd1084e8..2655de8e3 100644 --- a/postfix/src/util/known_tcp_ports_test.c +++ b/postfix/src/util/known_tcp_ports_test.c @@ -1,6 +1,6 @@ /* - * Test program to exercise known_tcp_ports.c. See ptest_main.h for a - * documented example. + * Test program to exercise known_tcp_ports.c. See PTEST_README for + * documentation. */ /* diff --git a/postfix/src/util/msg_output_test.c b/postfix/src/util/msg_output_test.c index 48c6402d8..cc7cb1dd7 100644 --- a/postfix/src/util/msg_output_test.c +++ b/postfix/src/util/msg_output_test.c @@ -1,6 +1,6 @@ /* - * Test program to exercise the msg_output module. See comments in - * ptest_main.h for documented examples. + * Test program to exercise the msg_output module. See PTEST_README for + * documentation. */ /* diff --git a/postfix/src/util/myaddrinfo_test.c b/postfix/src/util/myaddrinfo_test.c index 4a8cf0f7a..d334ee78d 100644 --- a/postfix/src/util/myaddrinfo_test.c +++ b/postfix/src/util/myaddrinfo_test.c @@ -1,8 +1,8 @@ /* * Test program for the myaddrinfo module. The purpose is to verify that the * myaddrinfo functions make the expected calls, and that they forward the - * expected results. See comments in ptest_main.h and pmock_expect_test.c - * for a documented example. + * expected results. See comments in pmock_expect_test.c, and PTEST_README + * for documentation. */ /* diff --git a/postfix/src/util/mymalloc_test.c b/postfix/src/util/mymalloc_test.c index fc3c8c036..a87743e60 100644 --- a/postfix/src/util/mymalloc_test.c +++ b/postfix/src/util/mymalloc_test.c @@ -1,7 +1,6 @@ /* - * Tests to verify malloc sanity checks. See comments in ptest_main.h for a - * documented example. The test code depends on the real mymalloc library, - * so we can 't do destructive tests. + * Tests to verify mymalloc sanity checks. See PTEST_README for + * documentation. */ /* @@ -221,7 +220,7 @@ static void test_mystrndup_static_empty(PTEST_CTX *t, got, want); /* - * myfree() is a NOOP. + * myfree() is a NOOP for "empty" mystrdup() or mystrndup() results. */ myfree(want); myfree(got); @@ -258,7 +257,7 @@ static void test_mymemdup_fatal_out_of_mem(PTEST_CTX *t, const PTEST_CASE *tp) ptest_fatal(t, "mymemdup(_, SSIZE_T_MAX-100) returned"); } -const PTEST_CASE ptestcases[] = { +static const PTEST_CASE ptestcases[] = { {"mymalloc + myfree normal case", test_mymalloc_normal, }, {"mymalloc panic for too small request", test_mymalloc_panic_too_small, diff --git a/postfix/src/util/mystrtok_test.c b/postfix/src/util/mystrtok_test.c index 9cc50a4a2..7f5800f95 100644 --- a/postfix/src/util/mystrtok_test.c +++ b/postfix/src/util/mystrtok_test.c @@ -1,6 +1,5 @@ /* - * Test program to exercise mystrtok.c. See ptest_main.h for a documented - * example. + * Test program to exercise mystrtok.c. See PTEST_README for documentation. */ /* diff --git a/postfix/src/util/unescape.c b/postfix/src/util/unescape.c index 4eacbba60..1e7519f89 100644 --- a/postfix/src/util/unescape.c +++ b/postfix/src/util/unescape.c @@ -52,6 +52,11 @@ /* IBM T.J. Watson Research /* P.O. Box 704 /* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA /*--*/ /* System library. */ diff --git a/postfix/src/util/unescape_test.c b/postfix/src/util/unescape_test.c index 9a22749a9..75fe523fd 100644 --- a/postfix/src/util/unescape_test.c +++ b/postfix/src/util/unescape_test.c @@ -1,6 +1,5 @@ /* - * Test program to exercise unescape.c. See ptest_main.h for a documented - * example. + * Test program to exercise unescape.c. See PTEST_README for documentation. */ /*