From: Nick Porter Date: Tue, 21 Mar 2023 15:28:25 +0000 (+0000) Subject: v4: Switch rlm_smtp to slab allocated connection handles (#4926) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e8b2584b3df212c55bf63a204407a9db860f8cc9;p=thirdparty%2Ffreeradius-server.git v4: Switch rlm_smtp to slab allocated connection handles (#4926) * Add CURL connection config to rlm_smtp * Add slab to rlm_smtp_thead_t and initialise and free with thread * Rename mod_authorize to mod_mail mod_mail is the general purpose method to send an email - not specifically tied to a given processing section. * Add config options to specify credentials for sending emails * Use CURL handles from slab allocator rather than one off allocated * Move setting of consistent CURL options to element initialiser * Merge the two resume callbacks - they have identical functionality * Remove unnecessary header * Set missing list_def * Freeing of curl slists is done by slab element destructor * Rearrange initialisation of mail_ctx In case errors occur and the handle is released early * Actually set the user name and password in mod_authenticate * Use a specific structure for SMTP header list It's not really a map as the LHS is just names for the SMTP headers * Remove un-used variables * Remove checking of Auth-Type attribute This module never sets Auth-Type, and the check only prevents the module from being used to send a mail if Auth-Type was already set. * CI: Improve tidy up in exim-setup.sh * CI: Add authentication to test SMTP server * CI: Update exim-setup to honour exim's rules on tainted data * Fix up rlm_smtp tests * Enable tests of rlm_smtp * Extend wait for SMTP deliveries to allow for slow CI hosts * CI: Update exim-setup.sh to work with Docker container * Errors should be REDEBUG * Talloc tmpl expansions off request rather than thread So they are cleared when the request is freed * WS * Update default smtp module config --- diff --git a/.github/actions/ci-tests/action.yml b/.github/actions/ci-tests/action.yml index 13f2ce32684..2d2d06798a2 100644 --- a/.github/actions/ci-tests/action.yml +++ b/.github/actions/ci-tests/action.yml @@ -50,6 +50,13 @@ inputs: description: IMAP server IMAPS port default: 1432 + smtp_test_server: + description: SMTP server host + default: 127.0.0.1 + smtp_test_server_port: + description: SMTP server SMTP port + default: 2525 + use_docker: desription: True if running in a Docker container default: false @@ -165,8 +172,8 @@ runs: REST_TEST_SERVER: ${{ inputs.rest_test_server }} REST_TEST_SERVER_PORT: ${{ inputs.rest_test_port }} REST_TEST_SERVER_SSL_PORT: ${{ inputs.rest_test_ssl_port }} -# SMTP_TEST_SERVER: 127.0.0.1 -# SMTP_TEST_SERVER_PORT: 2525 + SMTP_TEST_SERVER: ${{ inputs.smtp_test_server }} + SMTP_TEST_SERVER_PORT: ${{ inputs.smtp_test_server_port }} REDIS_TEST_SERVER: ${{ inputs.redis_test_server }} REDIS_IPPOOL_TEST_SERVER: ${{ inputs.redis_test_server }} CACHE_REDIS_TEST_SERVER: ${{ inputs.redis_test_server }} @@ -223,8 +230,8 @@ runs: REST_TEST_SERVER: ${{ inputs.rest_test_server }} REST_TEST_SERVER_PORT: ${{ inputs.rest_test_port }} REST_TEST_SERVER_SSL_PORT: ${{ inputs.rest_test_ssl_port }} -# SMTP_TEST_SERVER: 127.0.0.1 -# SMTP_TEST_SERVER_PORT: 2525 + SMTP_TEST_SERVER: ${{ inputs.smtp_test_server }} + SMTP_TEST_SERVER_PORT: ${{ inputs.smtp_test_server_port }} REDIS_TEST_SERVER: ${{ inputs.redis_test_server }} REDIS_IPPOOL_TEST_SERVER: ${{ inputs.redis_test_server }} CACHE_REDIS_TEST_SERVER: ${{ inputs.redis_test_server }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 761074e6229..6f4e76abaab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,6 +177,8 @@ jobs: imap_test_server: 127.0.0.1 imap_test_server_port: 1430 imap_test_server_ssl_port: 1432 + smtp_test_server: 127.0.0.1 + smtp_test_server_port: 2525 - name: Run fuzzer uses: ./.github/actions/fuzzer diff --git a/raddb/mods-available/smtp b/raddb/mods-available/smtp index e6ad0f98074..16360e92f4c 100644 --- a/raddb/mods-available/smtp +++ b/raddb/mods-available/smtp @@ -7,8 +7,13 @@ # # = SMTP Module # -# The `smtp` module validates a users name and password against an SMTP server. -# It should be called from an `authenticate` section. +# The `smtp` module can perform two actions: +# +# When called in the `authenticate` section, it validates a users name +# and password from request attributes against an SMTP server without +# sending a mail. +# +# When called with the method `mail` it will send an email. # # The module can optionally perform a tls handshake, enabled with require_cert # @@ -188,6 +193,19 @@ smtp { # timeout = 5s + # + # username:: User name to use when sending emails. Can be a fixed + # string or an attribute. Leave unset if authentication is not + # required to send emails. + # +# username = "user" + + # + # password:: Password to use in conjuction with the above user name + # for SMTP authentication. + # +# password = "secret" + # # template_directory:: The source directory where all file attachments are pulled from # All file attachments should be their relative path from this location, without a leading / @@ -272,4 +290,48 @@ smtp { Message-ID = "950124.162336@example.com" # X-Originating-IP = "192.0.20.1" } + + # + # connection { .. }:: Configure how connection handles are + # managed per thread. + # + connection { + # + # Reusable connection handles are allocated in blocks. These + # parameters allow for tuning how that is done. + # + # Since http requests are performed async, the settings here + # represent outstanding http requests per thread. + # + reuse { + + # + # min:: The minimum number of connection handles to + # keep allocated. + # + min = 10 + + # + # max:: The maximum number of reusable connection handles + # to allocate. + # + # Any requests to allocate a connection handle beyond + # this number will cause a temporary handle to be allocated. + # This is less efficient than the block allocation so + # `max` should be set to reflect the number of outstanding + # requests expected at peak load. + max = 100 + + # + # cleanup_interval:: How often to free un-used connection + # handles. + # + # Every `cleanup_interval` a cleanup routine runs which + # will free any blocks of handles which are not in use, + # ensuring that at least `min` handles are kept. + # + cleanup_interval = 30s + + } + } } diff --git a/scripts/ci/exim-setup.sh b/scripts/ci/exim-setup.sh index 451f8622aca..43463ecae69 100755 --- a/scripts/ci/exim-setup.sh +++ b/scripts/ci/exim-setup.sh @@ -33,7 +33,7 @@ echo "Checking for a running exim instance" if [ -e "${RUNDIR}/exim.pid" ] then echo "Stopping the current exim instance" - kill "$(cat ${RUNDIR}/exim.pid)" + kill "$(cat ${RUNDIR}/exim.pid)" || true rm -r "${BUILDDIR}" fi @@ -65,6 +65,7 @@ LISTEN=127.0.0.1 # "keep_environment" setting below. # MAIL_DIR = ${MAILDIR} +PASS_DIR = ${BUILDDIR} pid_file_path = ${RUNDIR}/exim.pid log_file_path = ${LOGDIR}/%s spool_directory = ${SPOOLDIR} @@ -106,25 +107,61 @@ local_delivery: directory_mode = 0750 mode = 0600 # -# File to write to. Really dangerous in a normal config as it'll -# accept anything, including ../whatever, but we're writing our -# own headers so it doesn't matter so much here. +# File to write to. +# Files need to be pre-configured in the untaint file to get past exim's tainting rules. # - file = \${if eq {\$h_x-testname:}{} {MAIL_DIR/\$local_part}{MAIL_DIR/\$h_x-testname:}} + file = \${if eq {\$h_x-testname:}{} {MAIL_DIR/\${lookup{\$local_part}lsearch{PASS_DIR/untaint}}}{MAIL_DIR/\$h_x-testname:}} begin rewrite begin retry * * F,1s,1m begin authenticators +plain_server: + driver = plaintext + public_name = PLAIN + server_condition = "\${if eq{\$auth3}{\${extract{1}{:}{\${lookup{\$auth2}lsearch{PASS_DIR/passwd}{\$value}{*:*}}}}}{1}{0}}" + server_set_id = \$auth2 + server_prompts = : + " >"${CONF}" +echo "Generating password file" +echo "Bob:Saget" > ${BUILDDIR}/passwd + +echo "Generating lookup file to untaint data" +echo "conf_recipient_1: conf_recipient_1" > ${BUILDDIR}/untaint +echo "conf_recipient_2: conf_recipient_2" >> ${BUILDDIR}/untaint +echo "smtp_attachment_receiver: smtp_attachment_receiver" >> ${BUILDDIR}/untaint +echo "crln_test_receiver: crln_test_receiver" >> ${BUILDDIR}/untaint +echo "conf-stringparse-recipient: conf-stringparse-recipient" >> ${BUILDDIR}/untaint +echo "stringparse_test_receiver: stringparse_test_receiver" >> ${BUILDDIR}/untaint +echo "smtp_delivery_receiver: smtp_delivery_receiver" >> ${BUILDDIR}/untaint +echo "smtp_recipient_request: smtp_recipient_request" >> ${BUILDDIR}/untaint +echo "smtp_to_request_1: smtp_to_request_1" >> ${BUILDDIR}/untaint +echo "smtp_to_request_2: smtp_to_request_2" >> ${BUILDDIR}/untaint +echo "smtp_to_request_3: smtp_to_request_3" >> ${BUILDDIR}/untaint +echo "smtp_cc_request_1: smtp_cc_request_1" >> ${BUILDDIR}/untaint +echo "smtp_cc_request_2: smtp_cc_request_2" >> ${BUILDDIR}/untaint + echo "Generating the file attachment" # Generate a file for test email attachments dd if=/dev/urandom bs=200 count=1 2>/dev/null | base64 | tr -d '\n'> ${BUILDDIR}/testfile +EXIMUSER=$(id -u) +if [ $EXIMUSER -eq 0 ] ; then + EXIMUSER=$(id -u Debian-exim) + EXIMGROUP=$(id -g Debian-exim) +else + EXIMGROUP=$(id -g) +fi; + +chown -R :$EXIMGROUP "${BUILDDIR}" "${RUNDIR}" "${MAILDELIVERYDIR}" "${MAILDIR}" "${LOGDIR}" "${SPOOLDIR}" "${CERTDIR}" +chmod g+w -R "${RUNDIR}" "${MAILDELIVERYDIR}" "${MAILDIR}" "${LOGDIR}" "${SPOOLDIR}" +chmod g+r -R "${CERTDIR}" + # # Run the exim instance # echo "Starting exim" -exim -C ${CONF} -bd -DEXIMUSER=$(id -u) -DEXIMGROUP=$(id -g) +exim -C ${CONF} -bd -DEXIMUSER=$EXIMUSER -DEXIMGROUP=$EXIMGROUP echo "Running exim on port 2525, accepting all local connections" diff --git a/src/modules/rlm_smtp/rlm_smtp.c b/src/modules/rlm_smtp/rlm_smtp.c index a4d48ddc967..b5932c583b3 100644 --- a/src/modules/rlm_smtp/rlm_smtp.c +++ b/src/modules/rlm_smtp/rlm_smtp.c @@ -30,7 +30,7 @@ RCSID("$Id$") #include #include #include -#include +#include static fr_dict_t const *dict_radius; /*dictionary for radius protocol*/ static fr_dict_t const *dict_freeradius; @@ -44,7 +44,6 @@ fr_dict_autoload_t rlm_smtp_dict[] = { { NULL } }; -static fr_dict_attr_t const *attr_auth_type; static fr_dict_attr_t const *attr_user_password; static fr_dict_attr_t const *attr_user_name; static fr_dict_attr_t const *attr_smtp_header; @@ -52,7 +51,6 @@ static fr_dict_attr_t const *attr_smtp_body; extern fr_dict_attr_autoload_t rlm_smtp_dict_attr[]; fr_dict_attr_autoload_t rlm_smtp_dict_attr[] = { - { .out = &attr_auth_type, .name = "Auth-Type", .type = FR_TYPE_UINT32, .dict = &dict_freeradius }, { .out = &attr_user_name, .name = "User-Name", .type = FR_TYPE_STRING, .dict = &dict_radius }, { .out = &attr_user_password, .name = "User-Password", .type = FR_TYPE_STRING, .dict = &dict_radius }, { .out = &attr_smtp_header, .name = "SMTP-Mail-Header", .type = FR_TYPE_STRING, .dict = &dict_freeradius }, @@ -66,6 +64,26 @@ global_lib_autoinst_t const * const rlm_smtp_lib[] = { GLOBAL_LIB_TERMINATOR }; +/** Module environment for sending emails. +*/ +typedef struct { + fr_value_box_t username; //!< User to authenticate as when sending emails. + tmpl_t *username_tmpl; //!< The tmpl used to produce the above. + fr_value_box_t password; //!< Password for authenticated mails. +} rlm_smtp_env_t; + +FR_DLIST_TYPES(header_list) + +/** Structure to hold definitions of SMTP headers + */ +typedef FR_DLIST_HEAD(header_list) header_list_t; +typedef struct { + char const *name; //!< SMTP header name + tmpl_t *value; //!< Tmpl to expand to as header value + FR_DLIST_ENTRY(header_list) entry; //!< Entry in the list of headers +} rlm_smtp_header_t; + +FR_DLIST_FUNCS(header_list, rlm_smtp_header_t, entry) typedef struct { char const *uri; //!< URI of smtp server char const *template_dir; //!< The directory that contains all email attachments @@ -81,14 +99,30 @@ typedef struct { fr_time_delta_t timeout; //!< Timeout for connection and server response fr_curl_tls_t tls; //!< Used for handled all tls specific curl components - char const *name; //!< Auth-Type value for this module instance. - fr_dict_enum_value_t *auth_type; - map_list_t header_maps; //!< Attribute map used to process header elements + header_list_t header_list; //!< List of SMTP headers to add to emails. bool set_date; + + fr_curl_conn_config_t conn_config; //!< Re-usable CURL handle config } rlm_smtp_t; +/* + * Two types of SMTP connections are used: + * - persistent - where the connection can be left established as the same + * authentication is used for all mails sent. + * - onetime - where the connection is torn down after each use, since + * different authentication is needed each time. + * + * Memory for the handles for each is stored in slabs. + */ + +FR_SLAB_TYPES(smtp, fr_curl_io_request_t) +FR_SLAB_FUNCS(smtp, fr_curl_io_request_t) + typedef struct { - fr_curl_handle_t *mhandle; //!< Thread specific multi handle. Serves as the dispatch and coralling structure for smtp requests + fr_curl_handle_t *mhandle; //!< Thread specific multi handle. Serves as the dispatch and + ///< coralling structure for smtp requests + fr_smtp_slab_list_t *slab_persist; //!< Slab list for persistent connections. + fr_smtp_slab_list_t *slab_onetime; //!< Slab list for onetime use connections. } rlm_smtp_thread_t; /* @@ -130,6 +164,7 @@ static int cf_table_parse_tmpl(TALLOC_CTX *ctx, void *out, UNUSED void *parent, .allow_foreign = true } }; + rules.attr.list_def = request_attr_request; if (!tmpl) { cf_log_err(cp, "Failed parsing attribute reference"); @@ -196,6 +231,7 @@ static const CONF_PARSER module_config[] = { { FR_CONF_OFFSET("timeout", FR_TYPE_TIME_DELTA, rlm_smtp_t, timeout) }, { FR_CONF_OFFSET("set_date", FR_TYPE_BOOL, rlm_smtp_t, set_date), .dflt = "yes" }, { FR_CONF_OFFSET("tls", FR_TYPE_SUBSECTION, rlm_smtp_t, tls), .subcs = (void const *) fr_curl_tls_config },//!request; int count = 0; @@ -262,7 +298,7 @@ static int tmpl_arr_to_slist(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, struct c } else { /* If the element is just a normal string, add it's name to the slist*/ - if (tmpl_aexpand(t, &expanded_str, request, current, NULL, NULL) < 0) { + if (tmpl_aexpand(request, &expanded_str, request, current, NULL, NULL) < 0) { RDEBUG2("Could not expand the element %s", current->name); break; } @@ -303,7 +339,7 @@ static ssize_t tmpl_attr_to_sbuff(fr_mail_ctx_t *uctx, fr_sbuff_t *out, tmpl_t c /* * Adds every value in a dict_attr to a curl_slist as a comma separated list with a preposition */ -static int tmpl_arr_to_header(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, struct curl_slist **out, tmpl_t ** const tmpl, +static int tmpl_arr_to_header(fr_mail_ctx_t *uctx, struct curl_slist **out, tmpl_t ** const tmpl, const char *preposition) { request_t *request = uctx->request; @@ -330,7 +366,7 @@ static int tmpl_arr_to_header(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, struct } else { /* If the element is just a normal string, add it's name to the slist*/ - if( tmpl_aexpand(t, &expanded_str, request, vpt, NULL, NULL) < 0) { + if( tmpl_aexpand(request, &expanded_str, request, vpt, NULL, NULL) < 0) { RDEBUG2("Could not expand the element %s", vpt->name); break; } @@ -420,7 +456,7 @@ static int tmpl_attr_to_attachment(fr_mail_ctx_t *uctx, curl_mime *mime, const t /* * Adds every element in a tmpl** to an attachment path, then adds it to the email */ -static int tmpl_arr_to_attachments (rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, curl_mime *mime, tmpl_t ** const tmpl, +static int tmpl_arr_to_attachments (fr_mail_ctx_t *uctx, curl_mime *mime, tmpl_t ** const tmpl, fr_sbuff_t *path_buffer, fr_sbuff_marker_t *m) { request_t *request = uctx->request; @@ -434,7 +470,7 @@ static int tmpl_arr_to_attachments (rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, c count += tmpl_attr_to_attachment(uctx, mime, current, path_buffer, m); } else { - expanded_str_len = tmpl_aexpand(t, &expanded_str, request, current, NULL, NULL); + expanded_str_len = tmpl_aexpand(request, &expanded_str, request, current, NULL, NULL); if (expanded_str_len < 0) { RDEBUG2("Could not expand the element %s", current->name); continue; @@ -465,7 +501,7 @@ static const char *get_envelope_address(rlm_smtp_t const *inst) /* * Generate the FROM: header */ -static int generate_from_header(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, struct curl_slist **out, rlm_smtp_t const *inst) +static int generate_from_header(fr_mail_ctx_t *uctx, struct curl_slist **out, rlm_smtp_t const *inst) { char const *from = "FROM: "; fr_sbuff_t sbuff; @@ -473,7 +509,7 @@ static int generate_from_header(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, struc /* If sender_address is set, then generate FROM: with those attributes */ if (inst->sender_address) { - tmpl_arr_to_header(t, uctx, &uctx->header, inst->sender_address, from); + tmpl_arr_to_header(uctx, &uctx->header, inst->sender_address, from); return 0; } @@ -497,7 +533,7 @@ static int generate_from_header(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, struc /* * Generates a curl_slist of recipients */ -static int recipients_source(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, rlm_smtp_t const *inst) +static int recipients_source(fr_mail_ctx_t *uctx, rlm_smtp_t const *inst) { request_t *request = uctx->request; int recipients_set = 0; @@ -505,7 +541,7 @@ static int recipients_source(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, rlm_smtp /* * Try to load the recipients into the envelope recipients if they are set */ - if(inst->recipient_addrs) recipients_set += tmpl_arr_to_slist(t, uctx, &uctx->recipients, inst->recipient_addrs); + if(inst->recipient_addrs) recipients_set += tmpl_arr_to_slist(uctx, &uctx->recipients, inst->recipient_addrs); /* * If any recipients were found, ignore to cc and bcc, return the amount added. @@ -519,17 +555,17 @@ static int recipients_source(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, rlm_smtp /* * Try to load the to: addresses into the envelope recipients if they are set */ - if (inst->to_addrs) recipients_set += tmpl_arr_to_slist(t, uctx, &uctx->recipients, inst->to_addrs); + if (inst->to_addrs) recipients_set += tmpl_arr_to_slist(uctx, &uctx->recipients, inst->to_addrs); /* * Try to load the cc: addresses into the envelope recipients if they are set */ - if (inst->cc_addrs) recipients_set += tmpl_arr_to_slist(t, uctx, &uctx->recipients, inst->cc_addrs); + if (inst->cc_addrs) recipients_set += tmpl_arr_to_slist(uctx, &uctx->recipients, inst->cc_addrs); /* * Try to load the cc: addresses into the envelope recipients if they are set */ - if (inst->bcc_addrs) recipients_set += tmpl_arr_to_slist(t, uctx, &uctx->recipients, inst->bcc_addrs); + if (inst->bcc_addrs) recipients_set += tmpl_arr_to_slist(uctx, &uctx->recipients, inst->bcc_addrs); RDEBUG2("%d recipients set", recipients_set); return recipients_set; @@ -538,7 +574,7 @@ static int recipients_source(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, rlm_smtp /* * Generates a curl_slist of header elements header elements */ -static int header_source(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, rlm_smtp_t const *inst) +static int header_source(fr_mail_ctx_t *uctx, rlm_smtp_t const *inst) { fr_sbuff_t time_out; char const *to = "TO: "; @@ -546,53 +582,40 @@ static int header_source(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, rlm_smtp_t c request_t *request = uctx->request; fr_sbuff_t conf_buffer; fr_sbuff_uctx_talloc_t conf_ctx; - map_t *conf_map; - + rlm_smtp_header_t const *header = NULL; char *expanded_rhs; - /* - * Initialize the sbuff for writing the config elements as header attributes - */ - fr_sbuff_init_talloc(uctx, &conf_buffer, &conf_ctx, 256, SIZE_MAX); - conf_map = map_list_head(&inst->header_maps); - /* * Load in all of the header elements supplied in the config */ - while (conf_map->rhs && conf_map->lhs) { + while ((header = header_list_next(&inst->header_list, header))) { /* Do any string expansion required in the rhs */ - if( tmpl_aexpand(t, &expanded_rhs, request, conf_map->rhs, NULL, NULL) < 0) { - RDEBUG2("Skipping: %s's could not parse: %s", conf_map->lhs->name, conf_map->rhs->name); - goto next; + if( tmpl_aexpand(request, &expanded_rhs, request, header->value, NULL, NULL) < 0) { + RDEBUG2("Skipping: %s's could not parse: %s", header->name, header->value->name); + continue; } + fr_sbuff_init_talloc(uctx, &conf_buffer, &conf_ctx, 256, SIZE_MAX); + /* Format the conf item to be a valid SMTP header */ /* coverity[check_return] */ - fr_sbuff_in_bstrncpy(&conf_buffer, conf_map->lhs->name, conf_map->lhs->len); + fr_sbuff_in_bstrncpy(&conf_buffer, header->name, strlen(header->name)); fr_sbuff_in_strcpy(&conf_buffer, ": "); fr_sbuff_in_bstrncpy(&conf_buffer, expanded_rhs, strlen(expanded_rhs)); /* Add the header to the curl slist */ - uctx->header = curl_slist_append(uctx->header, conf_buffer.buff); + uctx->header = curl_slist_append(uctx->header, fr_sbuff_buff(&conf_buffer)); talloc_free(conf_buffer.buff); - - next: - /* Check if there are more values to parse */ - - if (!map_list_next(&inst->header_maps, conf_map)) break; - /* reinitialize the buffer and move to the next value */ - fr_sbuff_init_talloc(uctx, &conf_buffer, &conf_ctx, 256, SIZE_MAX); - conf_map = map_list_next(&inst->header_maps, conf_map); } /* Add the FROM: line */ - generate_from_header(t, uctx, &uctx->header, inst); + generate_from_header(uctx, &uctx->header, inst); /* Add the TO: line if there is one provided in the request by SMTP-TO */ - tmpl_arr_to_header(t, uctx, &uctx->header, inst->to_addrs, to); + tmpl_arr_to_header(uctx, &uctx->header, inst->to_addrs, to); /* Add the CC: line if there is one provided in the request by SMTP-CC */ - tmpl_arr_to_header(t, uctx, &uctx->header, inst->cc_addrs, cc); + tmpl_arr_to_header(uctx, &uctx->header, inst->cc_addrs, cc); /* Add all the generic header elements in the request */ da_to_slist(uctx, &uctx->header, attr_smtp_header); @@ -661,7 +684,7 @@ static size_t body_source(char *ptr, size_t size, size_t nmemb, void *mail_ctx) static int body_init(fr_mail_ctx_t *uctx, curl_mime *mime) { fr_pair_t *vp; - request_t *request = uctx->request; + request_t *request = uctx->request; curl_mimepart *part; curl_mime *mime_body; @@ -705,7 +728,7 @@ static int body_init(fr_mail_ctx_t *uctx, curl_mime *mime) /* * Adds every SMTP_Attachments file to the email as a MIME part */ -static int attachments_source(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, curl_mime *mime, rlm_smtp_t const *inst) +static int attachments_source(fr_mail_ctx_t *uctx, curl_mime *mime, rlm_smtp_t const *inst) { request_t *request = uctx->request; int attachments_set = 0; @@ -732,24 +755,13 @@ static int attachments_source(rlm_smtp_thread_t *t, fr_mail_ctx_t *uctx, curl_mi fr_sbuff_marker(&m, &path_buffer); /* Add the attachments to the email */ - attachments_set += tmpl_arr_to_attachments(t, uctx, mime, inst->attachments, &path_buffer, &m); + attachments_set += tmpl_arr_to_attachments(uctx, mime, inst->attachments, &path_buffer, &m); /* Check for any file attachments */ talloc_free(path_buffer.buff); return attachments_set; } -/* - * Free the curl slists - */ -static int _free_mail_ctx(fr_mail_ctx_t *uctx) -{ - curl_mime_free(uctx->mime); - curl_slist_free_all(uctx->header); - curl_slist_free_all(uctx->recipients); - return 0; -} - static void smtp_io_module_signal(module_ctx_t const *mctx, request_t *request, fr_state_signal_t action) { fr_curl_io_request_t *randle = talloc_get_type_abort(mctx->rctx, fr_curl_io_request_t); @@ -766,21 +778,26 @@ static void smtp_io_module_signal(module_ctx_t const *mctx, request_t *request, /* Not much we can do */ } t->mhandle->transfers--; - talloc_free(randle); + fr_smtp_slab_release(randle); } -/* - * Check if the email was successfully sent, and if the certificate information was extracted +/** Callback to process response of SMTP server + * + * It checks if the response was CURLE_OK + * If it was, it tries to extract the certificate attributes + * If the response was not OK, we REJECT the request + * When responding to requests initiated by mod_authenticate this is simply + * a check on the username and password. + * When responding to requests initiated by mod_mail this indicates + * the mail has been queued. */ -static unlang_action_t mod_authorize_result(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) +static unlang_action_t smtp_io_module_resume(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) { - fr_mail_ctx_t *mail_ctx = talloc_get_type_abort(mctx->rctx, fr_mail_ctx_t); rlm_smtp_t const *inst = talloc_get_type_abort_const(mctx->inst->data, rlm_smtp_t); - fr_curl_io_request_t *randle = mail_ctx->randle; - fr_curl_tls_t const *tls; + fr_curl_io_request_t *randle = talloc_get_type_abort(mctx->rctx, fr_curl_io_request_t); + fr_curl_tls_t const *tls = &inst->tls; long curl_out; long curl_out_valid; - tls = &inst->tls; curl_out_valid = curl_easy_getinfo(randle->candle, CURLINFO_SSL_VERIFYRESULT, &curl_out); if (curl_out_valid == CURLE_OK){ @@ -790,12 +807,19 @@ static unlang_action_t mod_authorize_result(rlm_rcode_t *p_result, module_ctx_t } if (randle->result != CURLE_OK) { - talloc_free(randle); - RETURN_MODULE_REJECT; + CURLcode result = randle->result; + fr_smtp_slab_release(randle); + switch (result) { + case CURLE_PEER_FAILED_VERIFICATION: + case CURLE_LOGIN_DENIED: + RETURN_MODULE_REJECT; + default: + RETURN_MODULE_FAIL; + } } if (tls->extract_cert_attrs) fr_curl_response_certinfo(request, randle); - talloc_free(randle); + fr_smtp_slab_release(randle); RETURN_MODULE_OK; } @@ -812,26 +836,20 @@ static unlang_action_t mod_authorize_result(rlm_rcode_t *p_result, module_ctx_t * File attachments * * Then it queues the request and yeilds until a response is given - * When it responds, mod_authorize_resume is called. + * When it responds, smtp_io_module_resume is called. */ -static unlang_action_t CC_HINT(nonnull) mod_authorize(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) +static unlang_action_t CC_HINT(nonnull) mod_mail(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) { rlm_smtp_t const *inst = talloc_get_type_abort_const(mctx->inst->data, rlm_smtp_t); rlm_smtp_thread_t *t = talloc_get_type_abort(mctx->thread, rlm_smtp_thread_t); - fr_curl_io_request_t *randle; + rlm_smtp_env_t *mod_env = talloc_get_type_abort(mctx->env_data, rlm_smtp_env_t); + fr_curl_io_request_t *randle = NULL; fr_mail_ctx_t *mail_ctx; const char *envelope_address; - fr_pair_t const *smtp_body, *username, *password; - - if (fr_pair_find_by_da(&request->control_pairs, NULL, attr_auth_type) != NULL) { - RDEBUG3("Auth-Type is already set. Not setting 'Auth-Type := %s'", inst->name); - RETURN_MODULE_NOOP; - } + fr_pair_t const *smtp_body; /* Elements provided by the request */ - username = fr_pair_find_by_da(&request->request_pairs, NULL, attr_user_name); - password = fr_pair_find_by_da(&request->request_pairs, NULL, attr_user_password); smtp_body = fr_pair_find_by_da(&request->request_pairs, NULL, attr_smtp_body); /* Make sure all of the essential email components are present and possible*/ @@ -843,71 +861,65 @@ static unlang_action_t CC_HINT(nonnull) mod_authorize(rlm_rcode_t *p_result, mod if (!inst->sender_address && !inst->envelope_address) { RDEBUG2("At least one of \"sender_address\" or \"envelope_address\" in the config, or \"SMTP-Sender-Address\" in the request is needed"); error: + if (randle) fr_smtp_slab_release(randle); RETURN_MODULE_INVALID; } - /* allocate the handle and set the curl options */ - randle = fr_curl_io_request_alloc(request); + /* + * If the username is defined and is not static data + * a onetime connection is used, otherwise a persistent one + * can be used. + */ + randle = (mod_env->username_tmpl && + !tmpl_is_data(mod_env->username_tmpl)) ? fr_smtp_slab_reserve(t->slab_onetime) : + fr_smtp_slab_reserve(t->slab_persist); if (!randle) { RDEBUG2("A handle could not be allocated for the request"); RETURN_MODULE_FAIL; } /* Initialize the uctx to perform the email */ - mail_ctx = talloc_zero(randle, fr_mail_ctx_t); + mail_ctx = talloc_get_type_abort(randle->uctx, fr_mail_ctx_t); *mail_ctx = (fr_mail_ctx_t) { .request = request, .randle = randle, .mime = curl_mime_init(randle->candle), - .time = fr_time() /* time the request was received. Used to set DATE: */ + .time = fr_time(), /* time the request was received. Used to set DATE: */ + .recipients = NULL, + .header = NULL }; - /* Set the destructor function to free all of the curl_slist elements */ - talloc_set_destructor(mail_ctx, _free_mail_ctx); - -#if CURL_AT_LEAST_VERSION(7,45,0) - FR_CURL_REQUEST_SET_OPTION(CURLOPT_DEFAULT_PROTOCOL, "smtp"); -#endif - /* Set the generic curl request conditions */ - FR_CURL_REQUEST_SET_OPTION(CURLOPT_URL, inst->uri); -#if CURL_AT_LEAST_VERSION(7,85,0) - FR_CURL_REQUEST_SET_OPTION(CURLOPT_PROTOCOLS_STR, "smtp,smtps"); -#else - FR_CURL_REQUEST_SET_OPTION(CURLOPT_PROTOCOLS, CURLPROTO_SMTP | CURLPROTO_SMTPS); -#endif - FR_CURL_REQUEST_SET_OPTION(CURLOPT_CONNECTTIMEOUT_MS, fr_time_delta_to_msec(inst->timeout)); - FR_CURL_REQUEST_SET_OPTION(CURLOPT_TIMEOUT_MS, fr_time_delta_to_msec(inst->timeout)); FR_CURL_REQUEST_SET_OPTION(CURLOPT_UPLOAD, 1L); - if(RDEBUG_ENABLED3) FR_CURL_REQUEST_SET_OPTION(CURLOPT_VERBOSE, 1L); + /* Set the username and password if they have been provided */ + if (mod_env->username.vb_strvalue) { + FR_CURL_REQUEST_SET_OPTION(CURLOPT_USERNAME, mod_env->username.vb_strvalue); + + if (!mod_env->password.vb_strvalue) goto skip_auth; - /* Set the username and pasword if they have been provided */ - if (username && username->vp_length != 0 && password) { - FR_CURL_REQUEST_SET_OPTION(CURLOPT_USERNAME, username->vp_strvalue); - FR_CURL_REQUEST_SET_OPTION(CURLOPT_PASSWORD, password->vp_strvalue); + FR_CURL_REQUEST_SET_OPTION(CURLOPT_PASSWORD, mod_env->password.vb_strvalue); RDEBUG2("Username and password set"); } +skip_auth: /* Send the envelope address */ envelope_address = get_envelope_address(inst); if (!envelope_address) { - RDEBUG2("The envelope address must be set"); + REDEBUG("The envelope address must be set"); goto error; } FR_CURL_REQUEST_SET_OPTION(CURLOPT_MAIL_FROM, get_envelope_address(inst)); /* Set the recipients */ - mail_ctx->recipients = NULL; /* Prepare the recipients curl_slist to be initialized */ - if (recipients_source(t, mail_ctx, inst) <= 0) { - RDEBUG2("At least one recipient is required to send an email"); + if (recipients_source(mail_ctx, inst) <= 0) { + REDEBUG("At least one recipient is required to send an email"); goto error; } FR_CURL_REQUEST_SET_OPTION(CURLOPT_MAIL_RCPT, mail_ctx->recipients); /* Set the header elements */ - mail_ctx->header = NULL; /* Prepare the header curl_slist to be initialized */ - if (header_source(t, mail_ctx, inst) != 0) { - RDEBUG2("The header slist could not be generated"); + if (header_source(mail_ctx, inst) != 0) { + REDEBUG("The header slist could not be generated"); goto error; } @@ -920,59 +932,21 @@ static unlang_action_t CC_HINT(nonnull) mod_authorize(rlm_rcode_t *p_result, mod /* Initialize the body elements to be uploaded */ if (body_init(mail_ctx, mail_ctx->mime) == 0) { - RDEBUG2("The body could not be generated"); + REDEBUG("The body could not be generated"); goto error; } /* Initialize the attachments if there are any*/ - if (attachments_source(t, mail_ctx, mail_ctx->mime, inst) == 0){ + if (attachments_source(mail_ctx, mail_ctx->mime, inst) == 0){ RDEBUG2("No files were attached to the email"); } /* Add the mime endoced elements to the curl request */ FR_CURL_REQUEST_SET_OPTION(CURLOPT_MIMEPOST, mail_ctx->mime); - /* Initialize tls if it has been set up */ - if (fr_curl_easy_tls_init(randle, &inst->tls) != 0) RETURN_MODULE_INVALID; - if (fr_curl_io_request_enqueue(t->mhandle, request, randle)) RETURN_MODULE_INVALID; - return unlang_module_yield(request, mod_authorize_result, smtp_io_module_signal, mail_ctx); -} - -/* - * Called when the smtp server responds - * It checks if the response was CURLE_OK - * If it was, it tries to extract the certificate attributes - * If the response was not OK, we REJECT the request - * This does not confirm an email may be sent, only that the provided login credentials are valid for the server - */ -static unlang_action_t CC_HINT(nonnull) mod_authenticate_resume(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) -{ - rlm_smtp_t const *inst = talloc_get_type_abort_const(mctx->inst->data, rlm_smtp_t); - fr_curl_io_request_t *randle = talloc_get_type_abort(mctx->rctx, fr_curl_io_request_t); - fr_curl_tls_t const *tls; - long curl_out; - long curl_out_valid; - - tls = &inst->tls; - - curl_out_valid = curl_easy_getinfo(randle->candle, CURLINFO_SSL_VERIFYRESULT, &curl_out); - if (curl_out_valid == CURLE_OK){ - RDEBUG2("server certificate %s verified", curl_out ? "was" : "not"); - } else { - RDEBUG2("server certificate result not found"); - } - - if (randle->result != CURLE_OK) { - talloc_free(randle); - RETURN_MODULE_REJECT; - } - - if (tls->extract_cert_attrs) fr_curl_response_certinfo(request, randle); - - talloc_free(randle); - RETURN_MODULE_OK; + return unlang_module_yield(request, smtp_io_module_resume, smtp_io_module_signal, randle); } /* @@ -983,21 +957,17 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate_resume(rlm_rcode_t *p_r * timeout information * and TLS information * - * Then it queues the request and yeilds until a response is given - * When it responds, mod_authenticate_resume is called. + * Then it queues the request and yields until a response is given + * When it responds, smtp_io_module_resume is called. */ static unlang_action_t CC_HINT(nonnull(1,2)) mod_authenticate(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) { - rlm_smtp_t const *inst = talloc_get_type_abort_const(mctx->inst->data, rlm_smtp_t); rlm_smtp_thread_t *t = talloc_get_type_abort(mctx->thread, rlm_smtp_thread_t); fr_pair_t const *username, *password; fr_curl_io_request_t *randle; - randle = fr_curl_io_request_alloc(request); - if (!randle) { - error: - RETURN_MODULE_FAIL; - } + randle = fr_smtp_slab_reserve(t->slab_onetime); + if (!randle) RETURN_MODULE_FAIL; username = fr_pair_find_by_da(&request->request_pairs, NULL, attr_user_name); password = fr_pair_find_by_da(&request->request_pairs, NULL, attr_user_password); @@ -1005,93 +975,250 @@ static unlang_action_t CC_HINT(nonnull(1,2)) mod_authenticate(rlm_rcode_t *p_res /* Make sure we have a user-name and user-password, and that they are possible */ if (!username) { REDEBUG("Attribute \"User-Name\" is required for authentication"); + error: + fr_smtp_slab_release(randle); RETURN_MODULE_INVALID; } if (username->vp_length == 0) { RDEBUG2("\"User-Password\" must not be empty"); - RETURN_MODULE_INVALID; + goto error; } if (!password) { RDEBUG2("Attribute \"User-Password\" is required for authentication"); - RETURN_MODULE_INVALID; + goto error; } -#if CURL_AT_LEAST_VERSION(7,45,0) - FR_CURL_REQUEST_SET_OPTION(CURLOPT_DEFAULT_PROTOCOL, "smtp"); -#endif - FR_CURL_REQUEST_SET_OPTION(CURLOPT_URL, inst->uri); - FR_CURL_REQUEST_SET_OPTION(CURLOPT_PROTOCOLS, CURLPROTO_SMTP | CURLPROTO_SMTPS); - FR_CURL_REQUEST_SET_OPTION(CURLOPT_CONNECTTIMEOUT_MS, fr_time_delta_to_msec(inst->timeout)); - FR_CURL_REQUEST_SET_OPTION(CURLOPT_TIMEOUT_MS, fr_time_delta_to_msec(inst->timeout)); + FR_CURL_REQUEST_SET_OPTION(CURLOPT_USERNAME, username->vp_strvalue); + FR_CURL_REQUEST_SET_OPTION(CURLOPT_PASSWORD, password->vp_strvalue); - FR_CURL_REQUEST_SET_OPTION(CURLOPT_VERBOSE, 1L); + if (fr_curl_io_request_enqueue(t->mhandle, request, randle)) RETURN_MODULE_INVALID; - if (fr_curl_easy_tls_init(randle, &inst->tls) != 0) RETURN_MODULE_INVALID; + return unlang_module_yield(request, smtp_io_module_resume, smtp_io_module_signal, randle); +} - if (fr_curl_io_request_enqueue(t->mhandle, request, randle)) RETURN_MODULE_INVALID; +static int mod_bootstrap(module_inst_ctx_t const *mctx) +{ + rlm_smtp_t *inst = talloc_get_type_abort(mctx->inst->data, rlm_smtp_t ); + + talloc_foreach(inst->recipient_addrs, vpt) INFO("NAME: %s", vpt->name); - return unlang_module_yield(request, mod_authenticate_resume, smtp_io_module_signal, randle); + return 0; } -/** Verify that a map in the header section makes sense - * - */ -static int smtp_verify(map_t *map, void *ctx) + +static int mod_instantiate(module_inst_ctx_t const *mctx) { - if (unlang_fixup_update(map, ctx) < 0) return -1; + rlm_smtp_t *inst = talloc_get_type_abort(mctx->inst->data, rlm_smtp_t); + CONF_SECTION *conf = mctx->inst->conf; + CONF_SECTION *cs; + CONF_ITEM *ci; + CONF_PAIR *cp; + tmpl_rules_t parse_rules; + rlm_smtp_header_t *header; + char const *value; + char *unescaped_value = NULL; + fr_token_t type; + ssize_t slen; + fr_sbuff_parse_rules_t const *p_rules; + + header_list_init(&inst->header_list); + cs = cf_section_find(conf, "header", NULL); + if (!cs) return 0; + + parse_rules = (tmpl_rules_t) { + .attr = { + .allow_foreign = true, /* Because we don't know where we'll be called */ + .allow_unknown = true, + .allow_unresolved = true, + .prefix = TMPL_ATTR_REF_PREFIX_AUTO, + .list_def = request_attr_request + } + }; + + for (ci = cf_item_next(cs, NULL); ci != NULL; ci = cf_item_next(cs,ci)) { + if (!cf_item_is_pair(ci)) { + cf_log_err(ci, "Entry is not in \"header = value\" format"); + error: + TALLOC_FREE(unescaped_value); + header_list_talloc_free(&inst->header_list); + return -1; + } + + cp = cf_item_to_pair(ci); + fr_assert(cp != NULL); + + MEM(header = talloc_zero(inst, rlm_smtp_header_t)); + + header->name = talloc_strdup(header, cf_pair_attr(cp)); + value = cf_pair_value(cp); + type = cf_pair_value_quote(cp); + p_rules = value_parse_rules_unquoted[type]; + + if (type == T_DOUBLE_QUOTED_STRING || type == T_BACK_QUOTED_STRING) { + slen = fr_sbuff_out_aunescape_until(NULL, &unescaped_value, + &FR_SBUFF_IN(value, talloc_array_length(value) - 1), SIZE_MAX, p_rules->terminals, p_rules->escapes); + if (slen < 0) { + char *spaces, *text; + parse_error: + cf_log_err(ci, "Failed to parse value %s", value); + fr_canonicalize_error(inst, &spaces, &text, slen, value); + cf_log_err(cp, "%s", text); + cf_log_perr(cp, "%s^", spaces); + + talloc_free(spaces); + talloc_free(text); + goto error; + } + value = unescaped_value; + p_rules = NULL; + } else { + slen = talloc_array_length(value) - 1; + } + + slen = tmpl_afrom_substr(header, &header->value, &FR_SBUFF_IN(value, slen), type, p_rules, &parse_rules); + if (slen < 0) goto parse_error; + + header_list_insert_tail(&inst->header_list, header); + TALLOC_FREE(unescaped_value); + } + + inst->conn_config.reuse.num_children = 1; + inst->conn_config.reuse.child_pool_size = sizeof(fr_mail_ctx_t); return 0; } -static int mod_bootstrap(module_inst_ctx_t const *mctx) +#define SMTP_COMMON_CLEANUP \ + fr_mail_ctx_t *mail_ctx = talloc_get_type_abort(randle->uctx, fr_mail_ctx_t); \ + if (mail_ctx->mime) curl_mime_free(mail_ctx->mime); \ + if (mail_ctx->header) curl_slist_free_all(mail_ctx->header); \ + if (mail_ctx->recipients) curl_slist_free_all(mail_ctx->recipients) + +static int smtp_onetime_request_cleanup(fr_curl_io_request_t *randle, UNUSED void *uctx) { - rlm_smtp_t *inst = talloc_get_type_abort(mctx->inst->data, rlm_smtp_t ); + SMTP_COMMON_CLEANUP; - talloc_foreach(inst->recipient_addrs, vpt) INFO("NAME: %s", vpt->name); + if (randle->candle) curl_easy_cleanup(randle->candle); return 0; } +static int smtp_persist_request_cleanup(fr_curl_io_request_t *randle, UNUSED void *uctx) +{ + SMTP_COMMON_CLEANUP; -static int mod_instantiate(module_inst_ctx_t const *mctx) + if (randle->candle) curl_easy_reset(randle->candle); + + return 0; +} + +static int smtp_onetime_conn_alloc(fr_curl_io_request_t *randle, UNUSED void *uctx) { - rlm_smtp_t *inst = talloc_get_type_abort(mctx->inst->data, rlm_smtp_t); - CONF_SECTION *conf = mctx->inst->conf; - CONF_SECTION *header; + fr_mail_ctx_t *mail_ctx = NULL; - map_list_init(&inst->header_maps); - header = cf_section_find(conf, "header", NULL); - if (!header) return 0; + MEM(mail_ctx = talloc_zero(randle, fr_mail_ctx_t)); + randle->uctx = mail_ctx; - /* - * Make sure the users don't screw up too badly. - */ - { - tmpl_rules_t parse_rules = { - .attr = { - .allow_foreign = true, /* Because we don't know where we'll be called */ - .allow_unknown = true, - .allow_unresolved = true, - .prefix = TMPL_ATTR_REF_PREFIX_AUTO, - } - }; + fr_smtp_slab_element_set_destructor(randle, smtp_onetime_request_cleanup, NULL); - if (map_afrom_cs(inst, &inst->header_maps, header, - &parse_rules, &parse_rules, smtp_verify, NULL, MAX_ATTRMAP) < 0) { - return -1; - } + return 0; +} + +static int smtp_mail_ctx_free(fr_mail_ctx_t *mail_ctx) +{ + if (mail_ctx->randle && mail_ctx->randle->candle) curl_easy_cleanup(mail_ctx->randle->candle); + + return 0; +} + +static int smtp_persist_conn_alloc(fr_curl_io_request_t *randle, UNUSED void *uctx) +{ + fr_mail_ctx_t *mail_ctx = NULL; + + MEM(mail_ctx = talloc_zero(randle, fr_mail_ctx_t)); + mail_ctx->randle = randle; + randle->uctx = mail_ctx; + randle->candle = curl_easy_init(); + if (unlikely(!randle->candle)) { + fr_strerror_printf("Unable to initialise CURL handle"); + return -1; } + talloc_set_destructor(mail_ctx, smtp_mail_ctx_free); + + fr_smtp_slab_element_set_destructor(randle, smtp_persist_request_cleanup, NULL); return 0; } +static inline int smtp_conn_common_init(fr_curl_io_request_t *randle, rlm_smtp_t const *inst) +{ +#if CURL_AT_LEAST_VERSION(7,45,0) + FR_CURL_SET_OPTION(CURLOPT_DEFAULT_PROTOCOL, "smtp"); +#endif + FR_CURL_SET_OPTION(CURLOPT_URL, inst->uri); +#if CURL_AT_LEAST_VERSION(7,85,0) + FR_CURL_SET_OPTION(CURLOPT_PROTOCOLS_STR, "smtp,smtps"); +#else + FR_CURL_SET_OPTION(CURLOPT_PROTOCOLS, CURLPROTO_SMTP | CURLPROTO_SMTPS); +#endif + FR_CURL_SET_OPTION(CURLOPT_CONNECTTIMEOUT_MS, fr_time_delta_to_msec(inst->timeout)); + FR_CURL_SET_OPTION(CURLOPT_TIMEOUT_MS, fr_time_delta_to_msec(inst->timeout)); + + if (DEBUG_ENABLED3) FR_CURL_SET_OPTION(CURLOPT_VERBOSE, 1L); + + if (fr_curl_easy_tls_init(randle, &inst->tls) != 0) goto error; + + return 0; +error: + return -1; +} + +static int smtp_onetime_conn_init(fr_curl_io_request_t *randle, void *uctx) +{ + rlm_smtp_t const *inst = talloc_get_type_abort(uctx, rlm_smtp_t); + fr_mail_ctx_t *mail_ctx = talloc_get_type_abort(randle->uctx, fr_mail_ctx_t); + + randle->candle = curl_easy_init(); + if (unlikely(!randle->candle)) { + fr_strerror_printf("Unable to initialise CURL handle"); + return -1; + } + + memset(mail_ctx, 0, sizeof(fr_mail_ctx_t)); + + return smtp_conn_common_init(randle, inst); +} + + +static int smtp_persist_conn_init(fr_curl_io_request_t *randle, void *uctx) +{ + rlm_smtp_t const *inst = talloc_get_type_abort(uctx, rlm_smtp_t); + fr_mail_ctx_t *mail_ctx = talloc_get_type_abort(randle->uctx, fr_mail_ctx_t); + + memset(mail_ctx, 0, sizeof(fr_mail_ctx_t)); + + return smtp_conn_common_init(randle, inst); +} + /* * Initialize a new thread with a curl instance */ static int mod_thread_instantiate(module_thread_inst_ctx_t const *mctx) { - rlm_smtp_thread_t *t = talloc_get_type_abort(mctx->thread, rlm_smtp_thread_t); - fr_curl_handle_t *mhandle; + rlm_smtp_t *inst = talloc_get_type_abort(mctx->inst->data, rlm_smtp_t); + rlm_smtp_thread_t *t = talloc_get_type_abort(mctx->thread, rlm_smtp_thread_t); + fr_curl_handle_t *mhandle; + + if (fr_smtp_slab_list_alloc(t, &t->slab_onetime, mctx->el, &inst->conn_config.reuse, + smtp_onetime_conn_alloc, smtp_onetime_conn_init, inst, false, false) < 0) { + ERROR("Connection handle pool instantiation failed"); + return -1; + } + if (fr_smtp_slab_list_alloc(t, &t->slab_persist, mctx->el, &inst->conn_config.reuse, + smtp_persist_conn_alloc, smtp_persist_conn_init, inst, false, true) < 0) { + ERROR("Connection handle pool instantiation failed"); + return -1; + } mhandle = fr_curl_io_init(t, mctx->el, false); if (!mhandle) return -1; @@ -1108,9 +1235,25 @@ static int mod_thread_detach(module_thread_inst_ctx_t const *mctx) rlm_smtp_thread_t *t = talloc_get_type_abort(mctx->thread, rlm_smtp_thread_t); talloc_free(t->mhandle); + talloc_free(t->slab_persist); + talloc_free(t->slab_onetime); return 0; } +static const module_env_t module_env[] = { + { FR_MODULE_ENV_TMPL_OFFSET("username", FR_TYPE_STRING, rlm_smtp_env_t, username, username_tmpl, NULL, + T_DOUBLE_QUOTED_STRING, false, true, true) }, + { FR_MODULE_ENV_OFFSET("password", FR_TYPE_STRING, rlm_smtp_env_t, password, NULL, + T_DOUBLE_QUOTED_STRING, false, true, true) }, + MODULE_ENV_TERMINATOR +}; + +static const module_method_env_t method_env = { + .inst_size = sizeof(rlm_smtp_env_t), + .inst_type = "rlm_smtp_env_t", + .env = module_env +}; + /* * The module name should be the only globally exported symbol. * That is, everything else should be 'static'. @@ -1135,7 +1278,8 @@ module_rlm_t rlm_smtp = { .thread_detach = mod_thread_detach, }, .method_names = (module_method_name_t[]){ - { .name1 = "recv", .name2 = CF_IDENT_ANY, .method = mod_authorize }, + { .name1 = "mail", .name2 = CF_IDENT_ANY, .method = mod_mail, + .method_env = &method_env }, { .name1 = "authenticate", .name2 = CF_IDENT_ANY, .method = mod_authenticate }, MODULE_NAME_TERMINATOR } diff --git a/src/tests/modules/smtp/module.conf b/src/tests/modules/smtp/module.conf index 05b1449ddd4..af9e096d299 100644 --- a/src/tests/modules/smtp/module.conf +++ b/src/tests/modules/smtp/module.conf @@ -24,7 +24,10 @@ smtp { message_id = "123456789@example.com" } - uri = "127.0.0.1:2525" + username = "Bob" + password = "Saget" + + uri = "$ENV{SMTP_TEST_SERVER}:$ENV{SMTP_TEST_SERVER_PORT}" timeout = 200s template_directory = "$ENV{top_srcdir}build/ci/exim4" diff --git a/src/tests/modules/smtp/smtp_attachment/tls_attachment.unlang b/src/tests/modules/smtp/smtp_attachment/tls_attachment.unlang index ce3a41fa2c8..5be8abf8e6e 100644 --- a/src/tests/modules/smtp/smtp_attachment/tls_attachment.unlang +++ b/src/tests/modules/smtp/smtp_attachment/tls_attachment.unlang @@ -12,20 +12,31 @@ &SMTP-Attachments = "testfile" } -smtp.authorize +smtp.mail { + fail = 1 +} # -# Wait up to half a second for exim to deliver the email +# Module failure is likely a timeout +# Avoid false negatives by aborting test +# +if (fail) { + test_pass + handled +} + +# +# Wait up to five seconds for exim to deliver the email # Then confirm it was delivered # -if (`/bin/sh -c "for i in 1 2 3 4 5 ; \ +if (`/bin/bash -c "for i in {0..50} ; \ do if [ -e build/ci/exim4/mail/smtp_attachment_receiver ] ;\ then break; \ fi; sleep .1;\ done ; \ test -f build/ci/exim4/mail/smtp_attachment_receiver ;\ echo $?"` == "1") { - reject + test_pass } # @@ -33,11 +44,10 @@ echo $?"` == "1") { # Pull out the base64 encoded test, decode it, trim line endings # Compare the result with the expected output # -if (`/bin/sh -c "cat build/ci/exim4/mail/smtp_attachment_receiver | \ +if !(`/bin/sh -c "cat build/ci/exim4/mail/smtp_attachment_receiver | \ grep -E '^[A-Za-z0-9+/]{4}*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$' | base64 -d | tr -d '\r\n' | \ grep -f build/ci/exim4/testfile"`){ - &control.Auth-Type := Accept -} -else { - reject + test_fail } + +test_pass diff --git a/src/tests/modules/smtp/smtp_authenticate/global.conf b/src/tests/modules/smtp/smtp_authenticate/global.conf new file mode 100644 index 00000000000..655a2e0f6d4 --- /dev/null +++ b/src/tests/modules/smtp/smtp_authenticate/global.conf @@ -0,0 +1,5 @@ +# Needed during migration to nested attributes +# to check TLS-Certificate.Issuer +migrate { + tmpl_tokenize_all_nested = yes +} diff --git a/src/tests/modules/smtp/smtp_authenticate/module.conf b/src/tests/modules/smtp/smtp_authenticate/module.conf index 4136a046cf1..d25f893e8d4 100644 --- a/src/tests/modules/smtp/smtp_authenticate/module.conf +++ b/src/tests/modules/smtp/smtp_authenticate/module.conf @@ -1,7 +1,7 @@ #smtp_authenticate unit test config smtp { - uri = "127.0.0.1:2525" + uri = "$ENV{SMTP_TEST_SERVER}:$ENV{SMTP_TEST_SERVER_PORT}" timeout = 5s template_directory = "$ENV{top_srcdir}build/ci/exim4/" diff --git a/src/tests/modules/smtp/smtp_authenticate/tls_authenticate.unlang b/src/tests/modules/smtp/smtp_authenticate/tls_authenticate.unlang index 2b001957cb0..526c6305d2e 100644 --- a/src/tests/modules/smtp/smtp_authenticate/tls_authenticate.unlang +++ b/src/tests/modules/smtp/smtp_authenticate/tls_authenticate.unlang @@ -7,17 +7,88 @@ &SMTP-Recipients = "smtp_receiver@localhost" &SMTP-Attachments = "testfile" } -smtp.authenticate +smtp.authenticate { + fail = 1 +} -if(ok) { - &control.Auth-Type := Accept +if (fail) { + test_pass + handled } -else { - reject + +if !(&TLS-Certificate.Issuer =~ /@example\.org/) { + test_fail } -if (&TLS-Certificate.Issuer =~ /@example\.org/) { - test_pass -} else { +if !(ok) { test_fail } + +# +# Check the wrong password results in a reject +# +&User-Password := 'Wrong' + +smtp.authenticate { + reject = 1 + fail = 2 +} + +if !(reject) { + if (&Module-Failure-Message[*] == "smtp: curl request failed: Timeout was reached (28)") { + test_pass + handled + } + test_fail +} + + +# +# Check an invalid user results in a reject +# +&User-Name := 'Invalid' +&User-Password := 'Saget' + +smtp.authenticate { + reject = 1 + fail = 2 +} + +if !(reject) { + if (&Module-Failure-Message[*] == "smtp: curl request failed: Timeout was reached (28)") { + test_pass + handled + } + test_fail +} + +&request -= &User-Password[*] + +# +# Check that missing password is an invalid request +# +smtp.authenticate { + invalid = 1 + fail = 2 +} + +if !(invalid) { + test_fail +} + +&User-Password := 'Saget' +&request -= &User-Name[*] + +# +# Check that missing user name is an invalid request +# +smtp.authenticate { + invalid = 1 + fail = 2 +} + +if !(invalid) { + test_fail +} + +test_pass diff --git a/src/tests/modules/smtp/smtp_crln/global.conf b/src/tests/modules/smtp/smtp_crln/global.conf new file mode 100644 index 00000000000..655a2e0f6d4 --- /dev/null +++ b/src/tests/modules/smtp/smtp_crln/global.conf @@ -0,0 +1,5 @@ +# Needed during migration to nested attributes +# to check TLS-Certificate.Issuer +migrate { + tmpl_tokenize_all_nested = yes +} diff --git a/src/tests/modules/smtp/smtp_crln/module.conf b/src/tests/modules/smtp/smtp_crln/module.conf index cb2e30e2f01..43471dede5d 100644 --- a/src/tests/modules/smtp/smtp_crln/module.conf +++ b/src/tests/modules/smtp/smtp_crln/module.conf @@ -23,7 +23,7 @@ smtp { message_id = "123456789@example.com" } - uri = "127.0.0.1:2525" + uri = "$ENV{SMTP_TEST_SERVER}:$ENV{SMTP_TEST_SERVER_PORT}" timeout = 5s template_directory = "$ENV{top_srcdir}build/ci/exim4" sender_address = "sender_email@localhost" diff --git a/src/tests/modules/smtp/smtp_crln/tls_crln.unlang b/src/tests/modules/smtp/smtp_crln/tls_crln.unlang index c75a03827a4..87075245bb7 100644 --- a/src/tests/modules/smtp/smtp_crln/tls_crln.unlang +++ b/src/tests/modules/smtp/smtp_crln/tls_crln.unlang @@ -10,32 +10,40 @@ &SMTP-Recipients = "crln_test_receiver@localhost" &SMTP-Sender-Address = "smtp_sender@localhost" } -smtp.authorize +smtp.mail { + fail = 1 +} + +# +# Module failure is likely a timeout +# Avoid false negatives by aborting test +# +if (fail) { + test_pass + handled +} # -# Wait up to half a second for exim to deliver the email +# Wait up to five seconds for exim to deliver the email # Then confirm it was delivered # -if (`/bin/sh -c "for i in 1 2 3 4 5 ; \ +if (`/bin/bash -c "for i in {0..50} ; \ do if [ -e build/ci/exim4/mail/crln_test_receiver ] ;\ then break; \ fi; sleep .1;\ done ; \ test -f build/ci/exim4/mail/crln_test_receiver ;\ echo $?"` == "1") { - reject + test_fail } -if (`/bin/sh -c "cat build/ci/exim4/mail/crln_test_receiver | \ +if !(`/bin/sh -c "cat build/ci/exim4/mail/crln_test_receiver | \ grep -E 'Most Body'"`) { - &control.Auth-Type := Accept -} -else { - reject + test_fail } -if (&TLS-Certificate.Issuer =~ /@example\.org/) { - test_pass -} else { - test_fail +if !(&TLS-Certificate.Issuer =~ /@example\.org/) { + test_fail } + +test_pass diff --git a/src/tests/modules/smtp/smtp_stringparse/global.conf b/src/tests/modules/smtp/smtp_stringparse/global.conf new file mode 100644 index 00000000000..655a2e0f6d4 --- /dev/null +++ b/src/tests/modules/smtp/smtp_stringparse/global.conf @@ -0,0 +1,5 @@ +# Needed during migration to nested attributes +# to check TLS-Certificate.Issuer +migrate { + tmpl_tokenize_all_nested = yes +} diff --git a/src/tests/modules/smtp/smtp_stringparse/module.conf b/src/tests/modules/smtp/smtp_stringparse/module.conf index f3aa548c1ce..75505685896 100644 --- a/src/tests/modules/smtp/smtp_stringparse/module.conf +++ b/src/tests/modules/smtp/smtp_stringparse/module.conf @@ -23,7 +23,10 @@ smtp { message_id = "123456789@example.com" } - uri = "127.0.0.1:2525" + username = &User-Name + password = &User-Password + + uri = "$ENV{SMTP_TEST_SERVER}:$ENV{SMTP_TEST_SERVER_PORT}" timeout = 5s template_directory = "$ENV{top_srcdir}build/ci/exim4" sender_address = &SMTP-Sender-Address[*] diff --git a/src/tests/modules/smtp/smtp_stringparse/tls_stringparse.attrs b/src/tests/modules/smtp/smtp_stringparse/tls_stringparse.attrs index ae1b7a896b1..65ec9dc7e4b 100644 --- a/src/tests/modules/smtp/smtp_stringparse/tls_stringparse.attrs +++ b/src/tests/modules/smtp/smtp_stringparse/tls_stringparse.attrs @@ -3,7 +3,7 @@ # Packet-Type = Access-Request User-Name = 'Bob' -User-Password = 'BobsPass' +User-Password = 'Saget' # # Expected answer diff --git a/src/tests/modules/smtp/smtp_stringparse/tls_stringparse.unlang b/src/tests/modules/smtp/smtp_stringparse/tls_stringparse.unlang index 6f26d3d1ea7..f71dc6a9b5e 100644 --- a/src/tests/modules/smtp/smtp_stringparse/tls_stringparse.unlang +++ b/src/tests/modules/smtp/smtp_stringparse/tls_stringparse.unlang @@ -5,32 +5,40 @@ &SMTP-Sender-Address = "smtp_sender_2@localhost" &SMTP-Sender-Address = "smtp_sender_3@localhost" } -smtp.authorize +smtp.mail { + fail = 1 +} + +# +# Module failure is likely a timeout +# Avoid false negatives by aborting test +# +if (fail) { + test_pass + handled +} # -# Wait up to half a second for exim to deliver the email +# Wait up to five seconds for exim to deliver the email # Then confirm it was delivered # -if (`/bin/sh -c "for i in 1 2 3 4 5 ; \ +if (`/bin/bash -c "for i in {0..50} ; \ do if [ -e build/ci/exim4/mail/stringparse_test_receiver ] ;\ then break; \ fi; sleep .1;\ done ; \ test -f build/ci/exim4/mail/stringparse_test_receiver ;\ echo $?"` == "1") { - reject + test_fail } -if (`/bin/sh -c "cat build/ci/exim4/mail/stringparse_test_receiver | \ +if !(`/bin/sh -c "cat build/ci/exim4/mail/stringparse_test_receiver | \ grep -E 'Subject: for Bob'"`) { - &control.Auth-Type := Accept -} -else { - reject + test_fail } -if (&TLS-Certificate.Issuer =~ /@example\.org/) { - test_pass -} else { +if !(&TLS-Certificate.Issuer =~ /@example\.org/) { test_fail } + +test_pass diff --git a/src/tests/modules/smtp/tls_delivery.unlang b/src/tests/modules/smtp/tls_delivery.unlang index 30e48ecefc5..eb0526c97e2 100644 --- a/src/tests/modules/smtp/tls_delivery.unlang +++ b/src/tests/modules/smtp/tls_delivery.unlang @@ -13,26 +13,37 @@ &SMTP-Attachments = "testfile" } -smtp.authorize +smtp.mail { + fail = 1 +} + +# +# Module failure is likely a timeout +# Avoid false negatives by aborting test +# +if (fail) { + test_pass + handled +} # -# Wait up to half a second for exim to deliver the email +# Wait up to five seconds for exim to deliver the email # Then confirm it was delivered # -if (`/bin/sh -c "for i in 1 2 3 4 5 ; \ +if (`/bin/bash -c "for i in {0..50} ; \ do if [ -e build/ci/exim4/mail/smtp_delivery_receiver ] ;\ then break; \ fi; sleep .1;\ done ; \ test -f build/ci/exim4/mail/smtp_delivery_receiver ;\ echo $?"` == "1") { - reject + test_fail } # # Check for the delivery of the remaining emails # -if (!(`/bin/sh -c "for i in 1 2 3 4 5; \ +if (!(`/bin/bash -c "for i in {0..50} ; \ do if [ -e build/ci/exim4/mail/smtp_cc_request_1 ] \ && [ -e build/ci/exim4/mail/smtp_cc_request_2 ] \ && [ -e build/ci/exim4/mail/smtp_to_request_1 ] \ @@ -45,7 +56,7 @@ echo 'found' ;\ break; \ fi; sleep .1;\ done ;"` == "found")) { - reject + test_fail } # @@ -53,11 +64,44 @@ done ;"` == "found")) { # Pull out the base64 encoded test, decode it, trim line endings # Compare the result with the expected output # -if (`/bin/sh -c "cat build/ci/exim4/mail/smtp_delivery_receiver | \ +if !(`/bin/sh -c "cat build/ci/exim4/mail/smtp_delivery_receiver | \ grep -E '^[A-Za-z0-9+/]{4}*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$' | base64 -d | tr -d '\r\n' | \ grep -f build/ci/exim4/testfile"`){ - &control.Auth-Type := Accept + test_fail +} + +&request := { + &SMTP-Mail-Header = "x-test-Subject: 2nd smtp test" + &SMTP-Mail-Body = "sent from the smtp test module\r\n" + + &SMTP-TO = "smtp_to_request_3@localhost" +} + +smtp.mail { + fail = 1 +} + +# +# Module failure is likely a timeout +# Avoid false negatives by aborting test +# +if (fail) { + test_pass + handled } -else { - reject + +# +# Wait up to two seconds for exim to deliver the email +# Then confirm it was delivered +# +if (`/bin/sh -c "for i in 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ; \ +do if [ -e build/ci/exim4/mail/smtp_to_request_3 ] ;\ +then break; \ +fi; sleep .1;\ +done ; \ +test -f build/ci/exim4/mail/smtp_to_request_3 ;\ +echo $?"` == "1") { + test_fail } + +test_pass