X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/87ba3f5f78da0d53bfb3bdef9db569c8da241eba..8800895ae8a1e9c49c739839a6623292d7a473d0:/src/src/acl.c diff --git a/src/src/acl.c b/src/src/acl.c index 357abfad3..06fa6e898 100644 --- a/src/src/acl.c +++ b/src/src/acl.c @@ -1,4 +1,4 @@ -/* $Cambridge: exim/src/src/acl.c,v 1.32 2005/05/17 15:00:04 ph10 Exp $ */ +/* $Cambridge: exim/src/src/acl.c,v 1.47 2005/09/12 10:08:54 ph10 Exp $ */ /************************************************* * Exim - an Internet mail transport agent * @@ -63,6 +63,7 @@ ACLC_CONDITION, ACLC_CONTROL, #ifdef WITH_CONTENT_SCAN ACLC_MIME_REGEX, #endif + ACLC_RATELIMIT, ACLC_RECIPIENTS, #ifdef WITH_CONTENT_SCAN ACLC_REGEX, @@ -80,7 +81,9 @@ ACLC_CONDITION, ACLC_CONTROL, "log_message", "logwrite", and "set" are modifiers that look like conditions but always return TRUE. They are used for their side effects. */ -static uschar *conditions[] = { US"acl", US"authenticated", +static uschar *conditions[] = { + US"acl", + US"authenticated", #ifdef EXPERIMENTAL_BRIGHTMAIL US"bmi_optin", #endif @@ -110,6 +113,7 @@ static uschar *conditions[] = { US"acl", US"authenticated", #ifdef WITH_CONTENT_SCAN US"mime_regex", #endif + US"ratelimit", US"recipients", #ifdef WITH_CONTENT_SCAN US"regex", @@ -123,11 +127,45 @@ static uschar *conditions[] = { US"acl", US"authenticated", #endif US"verify" }; -/* ACL control names */ -static uschar *controls[] = { US"error", US"caseful_local_part", +/* Return values from decode_control(); keep in step with the table of names +that follows! */ + +enum { +#ifdef EXPERIMENTAL_BRIGHTMAIL + CONTROL_BMI_RUN, +#endif +#ifdef EXPERIMENTAL_DOMAINKEYS + CONTROL_DK_VERIFY, +#endif + CONTROL_ERROR, CONTROL_CASEFUL_LOCAL_PART, CONTROL_CASELOWER_LOCAL_PART, + CONTROL_ENFORCE_SYNC, CONTROL_NO_ENFORCE_SYNC, CONTROL_FREEZE, + CONTROL_QUEUE_ONLY, CONTROL_SUBMISSION, CONTROL_SUPPRESS_LOCAL_FIXUPS, +#ifdef WITH_CONTENT_SCAN + CONTROL_NO_MBOX_UNSPOOL, +#endif + CONTROL_FAKEDEFER, CONTROL_FAKEREJECT, CONTROL_NO_MULTILINE }; + +/* ACL control names; keep in step with the table above! This list is used for +turning ids into names. The actual list of recognized names is in the variable +control_def controls_list[] below. The fact that there are two lists is a mess +and should be tidied up. */ + +static uschar *controls[] = { + #ifdef EXPERIMENTAL_BRIGHTMAIL + US"bmi_run", + #endif + #ifdef EXPERIMENTAL_DOMAINKEYS + US"dk_verify", + #endif + US"error", US"caseful_local_part", US"caselower_local_part", US"enforce_sync", US"no_enforce_sync", US"freeze", - US"queue_only", US"submission", US"no_multiline"}; + US"queue_only", US"submission", US"suppress_local_fixups", + #ifdef WITH_CONTENT_SCAN + US"no_mbox_unspool", + #endif + + US"no_multiline"}; /* Flags to indicate for which conditions /modifiers a string expansion is done at the outer level. In the other cases, expansion already occurs in the @@ -171,6 +209,7 @@ static uschar cond_expand_at_top[] = { #ifdef WITH_CONTENT_SCAN TRUE, /* mime_regex */ #endif + TRUE, /* ratelimit */ FALSE, /* recipients */ #ifdef WITH_CONTENT_SCAN TRUE, /* regex */ @@ -227,6 +266,7 @@ static uschar cond_modifiers[] = { #ifdef WITH_CONTENT_SCAN FALSE, /* mime_regex */ #endif + FALSE, /* ratelimit */ FALSE, /* recipients */ #ifdef WITH_CONTENT_SCAN FALSE, /* regex */ @@ -363,6 +403,8 @@ static unsigned int cond_forbids[] = { ~(1<name, domain); /* Now we are ready to do the actual DNS lookup(s). */ +found = domain; switch (dns_special_lookup(&dnsa, domain, T_CSA, &found)) { /* If something bad happened (most commonly DNS_AGAIN), defer. */ @@ -1310,6 +1347,7 @@ BOOL verify_header_sender = FALSE; BOOL defer_ok = FALSE; BOOL callout_defer_ok = FALSE; BOOL no_details = FALSE; +BOOL success_on_redirect = FALSE; address_item *sender_vaddr = NULL; uschar *verify_sender_address = NULL; uschar *pm_mailfrom = NULL; @@ -1347,12 +1385,16 @@ if (strcmpic(ss, US"certificate") == 0) return FAIL; } -/* We can test the result of optional HELO verification */ +/* We can test the result of optional HELO verification that might have +occurred earlier. If not, we can attempt the verification now. */ if (strcmpic(ss, US"helo") == 0) { if (slash != NULL) goto NO_OPTIONS; - return helo_verified? OK : FAIL; + if (helo_verified) return OK; + if (helo_verify_failed) return FAIL; + if (smtp_verify_helo()) return helo_verified? OK : FAIL; + return DEFER; } /* Do Client SMTP Authorization checks in a separate function, and turn the @@ -1376,18 +1418,29 @@ always). */ if (strcmpic(ss, US"header_syntax") == 0) { if (slash != NULL) goto NO_OPTIONS; - if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) - { - *log_msgptr = string_sprintf("cannot check header contents in ACL for %s " - "(only possible in ACL for DATA)", acl_wherenames[where]); - return ERROR; - } + if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) goto WRONG_ACL; rc = verify_check_headers(log_msgptr); if (rc != OK && smtp_return_error_details && *log_msgptr != NULL) *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr); return rc; } +/* Check that no recipient of this message is "blind", that is, every envelope +recipient must be mentioned in either To: or Cc:. */ + +if (strcmpic(ss, US"not_blind") == 0) + { + if (slash != NULL) goto NO_OPTIONS; + if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) goto WRONG_ACL; + rc = verify_check_notblind(); + if (rc != OK) + { + *log_msgptr = string_sprintf("bcc recipient detected"); + if (smtp_return_error_details) + *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr); + } + return rc; + } /* The remaining verification tests check recipient and sender addresses, either from the envelope or from the header. There are a number of @@ -1400,12 +1453,7 @@ sender and recipient. */ if (strcmpic(ss, US"header_sender") == 0) { - if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) - { - *log_msgptr = string_sprintf("cannot check header contents in ACL for %s " - "(only possible in ACL for DATA)", acl_wherenames[where]); - return ERROR; - } + if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) goto WRONG_ACL; verify_header_sender = TRUE; } @@ -1452,6 +1500,7 @@ while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size)) { if (strcmpic(ss, US"defer_ok") == 0) defer_ok = TRUE; else if (strcmpic(ss, US"no_details") == 0) no_details = TRUE; + else if (strcmpic(ss, US"success_on_redirect") == 0) success_on_redirect = TRUE; /* These two old options are left for backwards compatibility */ @@ -1500,6 +1549,11 @@ while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size)) else if (strcmpic(opt, US"use_postmaster") == 0) verify_options |= vopt_callout_recippmaster; else if (strcmpic(opt, US"postmaster") == 0) pm_mailfrom = US""; + else if (strcmpic(opt, US"fullpostmaster") == 0) + { + pm_mailfrom = US""; + verify_options |= vopt_callout_fullpm; + } else if (strncmpic(opt, US"mailfrom", 8) == 0) { @@ -1704,6 +1758,9 @@ else if (verify_sender_address != NULL) else verify_options |= vopt_fake_sender; + if (success_on_redirect) + verify_options |= vopt_success_on_redirect; + /* The recipient, qualify, and expn options are never set in verify_options. */ @@ -1755,6 +1812,9 @@ else { address_item addr2; + if (success_on_redirect) + verify_options |= vopt_success_on_redirect; + /* We must use a copy of the address for verification, because it might get rewritten. */ @@ -1830,6 +1890,13 @@ NO_OPTIONS: *log_msgptr = string_sprintf("unexpected '/' found in \"%s\" " "(this verify item has no options)", arg); return ERROR; + +/* Calls in the wrong ACL come here */ + +WRONG_ACL: +*log_msgptr = string_sprintf("cannot check header contents in ACL for %s " + "(only possible in ACL for DATA)", acl_wherenames[where]); +return ERROR; } @@ -1877,6 +1944,293 @@ return d->value; +/************************************************* +* Handle rate limiting * +*************************************************/ + +/* Called by acl_check_condition() below to calculate the result +of the ACL ratelimit condition. + +Note that the return value might be slightly unexpected: if the +sender's rate is above the limit then the result is OK. This is +similar to the dnslists condition, and is so that you can write +ACL clauses like: defer ratelimit = 15 / 1h + +Arguments: + arg the option string for ratelimit= + log_msgptr for error messages + +Returns: OK - Sender's rate is above limit + FAIL - Sender's rate is below limit + DEFER - Problem opening ratelimit database + ERROR - Syntax error in options. +*/ + +static int +acl_ratelimit(uschar *arg, uschar **log_msgptr) +{ +double limit, period; +uschar *ss, *key; +int sep = '/'; +BOOL have_key = FALSE, leaky = FALSE, strict = FALSE; +BOOL per_byte = FALSE, per_cmd = FALSE, per_conn = FALSE, per_mail = FALSE; +int old_pool, rc; +tree_node **anchor, *t; +open_db dbblock, *dbm; +dbdata_ratelimit *dbd; +struct timeval tv; + +/* Parse the first two options and record their values in expansion +variables. These variables allow the configuration to have informative +error messages based on rate limits obtained from a table lookup. */ + +/* First is the maximum number of messages per period and maximum burst +size, which must be greater than or equal to zero. Zero is useful for +rate measurement as opposed to rate limiting. */ + +sender_rate_limit = string_nextinlist(&arg, &sep, NULL, 0); +if (sender_rate_limit == NULL) + limit = -1.0; +else + { + limit = Ustrtod(sender_rate_limit, &ss); + if (tolower(*ss) == 'k') { limit *= 1024.0; ss++; } + else if (tolower(*ss) == 'm') { limit *= 1024.0*1024.0; ss++; } + else if (tolower(*ss) == 'g') { limit *= 1024.0*1024.0*1024.0; ss++; } + } +if (limit < 0.0 || *ss != 0) + { + *log_msgptr = string_sprintf("syntax error in argument for " + "\"ratelimit\" condition: \"%s\" is not a positive number", + sender_rate_limit); + return ERROR; + } + +/* We use the rest of the argument list following the limit as the +lookup key, because it doesn't make sense to use the same stored data +if the period or options are different. */ + +key = arg; + +/* Second is the rate measurement period and exponential smoothing time +constant. This must be strictly greater than zero, because zero leads to +run-time division errors. */ + +sender_rate_period = string_nextinlist(&arg, &sep, NULL, 0); +if (sender_rate_period == NULL) period = -1.0; +else period = readconf_readtime(sender_rate_period, 0, FALSE); +if (period <= 0.0) + { + *log_msgptr = string_sprintf("syntax error in argument for " + "\"ratelimit\" condition: \"%s\" is not a time value", + sender_rate_period); + return ERROR; + } + +/* Parse the other options. Should we check if the per_* options are being +used in ACLs where they don't make sense, e.g. per_mail in the connect ACL? */ + +while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size)) + != NULL) + { + if (strcmpic(ss, US"leaky") == 0) leaky = TRUE; + else if (strcmpic(ss, US"strict") == 0) strict = TRUE; + else if (strcmpic(ss, US"per_byte") == 0) per_byte = TRUE; + else if (strcmpic(ss, US"per_cmd") == 0) per_cmd = TRUE; + else if (strcmpic(ss, US"per_conn") == 0) per_conn = TRUE; + else if (strcmpic(ss, US"per_mail") == 0) per_mail = TRUE; + else if (strcmpic(ss, US"per_rcpt") == 0) per_cmd = TRUE; /* alias */ + else have_key = TRUE; + } +if (leaky + strict > 1 || per_byte + per_cmd + per_conn + per_mail > 1) + { + *log_msgptr = US"conflicting options for \"ratelimit\" condition"; + return ERROR; + } + +/* Default option values */ +if (!strict) leaky = TRUE; +if (!per_byte && !per_cmd && !per_conn) per_mail = TRUE; + +/* If there is no explicit key, use the sender_host_address. If there is no +sender_host_address (e.g. -bs or acl_not_smtp) then we simply omit it. */ + +if (!have_key && sender_host_address != NULL) + key = string_sprintf("%s / %s", key, sender_host_address); + +HDEBUG(D_acl) debug_printf("ratelimit condition limit=%.0f period=%.0f key=%s\n", + limit, period, key); + +/* See if we have already computed the rate by looking in the relevant tree. For +per-connection rate limiting, store tree nodes and dbdata in the permanent pool +so that they survive across resets. */ + +anchor = NULL; +old_pool = store_pool; + +if (per_conn) + { + anchor = &ratelimiters_conn; + store_pool = POOL_PERM; + } +else if (per_mail || per_byte) + anchor = &ratelimiters_mail; +else if (per_cmd) + anchor = &ratelimiters_cmd; + +if (anchor != NULL && (t = tree_search(*anchor, key)) != NULL) + { + dbd = t->data.ptr; + /* The following few lines duplicate some of the code below. */ + if (dbd->rate < limit) rc = FAIL; + else rc = OK; + store_pool = old_pool; + sender_rate = string_sprintf("%.1f", dbd->rate); + HDEBUG(D_acl) + debug_printf("ratelimit found pre-computed rate %s\n", sender_rate); + return rc; + } + +/* We aren't using a pre-computed rate, so get a previously recorded +rate from the database, update it, and write it back. If there's no +previous rate for this key, create one. */ + +dbm = dbfn_open(US"ratelimit", O_RDWR, &dbblock, TRUE); +if (dbm == NULL) + { + store_pool = old_pool; + sender_rate = NULL; + HDEBUG(D_acl) debug_printf("ratelimit database not available\n"); + *log_msgptr = US"ratelimit database not available"; + return DEFER; + } +dbd = dbfn_read(dbm, key); + +gettimeofday(&tv, NULL); + +if (dbd == NULL) + { + HDEBUG(D_acl) debug_printf("ratelimit initializing new key's data\n"); + dbd = store_get(sizeof(dbdata_ratelimit)); + dbd->time_stamp = tv.tv_sec; + dbd->time_usec = tv.tv_usec; + dbd->rate = 0.0; + } +else + { + /* The smoothed rate is computed using an exponentially weighted moving + average adjusted for variable sampling intervals. The standard EWMA for + a fixed sampling interval is: f'(t) = (1 - a) * f(t) + a * f'(t - 1) + where f() is the measured value and f'() is the smoothed value. + + Old data decays out of the smoothed value exponentially, such that data n + samples old is multiplied by a^n. The exponential decay time constant p + is defined such that data p samples old is multiplied by 1/e, which means + that a = exp(-1/p). We can maintain the same time constant for a variable + sampling interval i by using a = exp(-i/p). + + The rate we are measuring is messages per period, suitable for directly + comparing with the limit. The average rate between now and the previous + message is period / interval, which we feed into the EWMA as the sample. + + It turns out that the number of messages required for the smoothed rate + to reach the limit when they are sent in a burst is equal to the limit. + This can be seen by analysing the value of the smoothed rate after N + messages sent at even intervals. Let k = (1 - a) * p/i + + rate_1 = (1 - a) * p/i + a * rate_0 + = k + a * rate_0 + rate_2 = k + a * rate_1 + = k + a * k + a^2 * rate_0 + rate_3 = k + a * k + a^2 * k + a^3 * rate_0 + rate_N = rate_0 * a^N + k * SUM(x=0..N-1)(a^x) + = rate_0 * a^N + k * (1 - a^N) / (1 - a) + = rate_0 * a^N + p/i * (1 - a^N) + + When N is large, a^N -> 0 so rate_N -> p/i as desired. + + rate_N = p/i + (rate_0 - p/i) * a^N + a^N = (rate_N - p/i) / (rate_0 - p/i) + N * -i/p = log((rate_N - p/i) / (rate_0 - p/i)) + N = p/i * log((rate_0 - p/i) / (rate_N - p/i)) + + Numerical analysis of the above equation, setting the computed rate to + increase from rate_0 = 0 to rate_N = limit, shows that for large sending + rates, p/i, the number of messages N = limit. So limit serves as both the + maximum rate measured in messages per period, and the maximum number of + messages that can be sent in a fast burst. */ + + double this_time = (double)tv.tv_sec + + (double)tv.tv_usec / 1000000.0; + double prev_time = (double)dbd->time_stamp + + (double)dbd->time_usec / 1000000.0; + double interval = this_time - prev_time; + + double i_over_p = interval / period; + double a = exp(-i_over_p); + + /* We must avoid division by zero, and deal gracefully with the clock going + backwards. If we blunder ahead when time is in reverse then the computed + rate will become bogusly huge. Clamp i/p to a very small number instead. */ + + if (i_over_p <= 0.0) i_over_p = 1e-9; + + dbd->time_stamp = tv.tv_sec; + dbd->time_usec = tv.tv_usec; + + /* If we are measuring the rate in bytes per period, multiply the + measured rate by the message size. If we don't know the message size + then it's safe to just use a value of zero and let the recorded rate + decay as if nothing happened. */ + + if (per_byte) + dbd->rate = (message_size < 0 ? 0.0 : (double)message_size) + * (1 - a) / i_over_p + a * dbd->rate; + else + dbd->rate = (1 - a) / i_over_p + a * dbd->rate; + } + +/* Clients sending at the limit are considered to be over the limit. This +matters for edge cases such the first message sent by a client (which gets +the initial rate of 0.0) when the rate limit is zero (i.e. the client should +be completely blocked). */ + +if (dbd->rate < limit) rc = FAIL; + else rc = OK; + +/* Update the state if the rate is low or if we are being strict. If we +are in leaky mode and the sender's rate is too high, we do not update +the recorded rate in order to avoid an over-aggressive sender's retry +rate preventing them from getting any email through. */ + +if (rc == FAIL || !leaky) + dbfn_write(dbm, key, dbd, sizeof(dbdata_ratelimit)); +dbfn_close(dbm); + +/* Store the result in the tree for future reference, if necessary. */ + +if (anchor != NULL) + { + t = store_get(sizeof(tree_node) + Ustrlen(key)); + t->data.ptr = dbd; + Ustrcpy(t->name, key); + (void)tree_insertnode(anchor, t); + } + +/* We create the formatted version of the sender's rate very late in +order to ensure that it is done using the correct storage pool. */ + +store_pool = old_pool; +sender_rate = string_sprintf("%.1f", dbd->rate); + +HDEBUG(D_acl) + debug_printf("ratelimit computed rate %s\n", sender_rate); + +return rc; +} + + + /************************************************* * Handle conditions/modifiers on an ACL item * *************************************************/ @@ -2101,19 +2455,20 @@ for (; cb != NULL; cb = cb->next) no_multiline_responses = TRUE; break; + case CONTROL_FAKEDEFER: case CONTROL_FAKEREJECT: - fake_reject = TRUE; + fake_response = (control_type == CONTROL_FAKEDEFER) ? DEFER : FAIL; if (*p == '/') { uschar *pp = p + 1; while (*pp != 0) pp++; - fake_reject_text = expand_string(string_copyn(p+1, pp-p-1)); + fake_response_text = expand_string(string_copyn(p+1, pp-p-1)); p = pp; } else { /* Explicitly reset to default string */ - fake_reject_text = US"Your message has been rejected but is being kept for evaluation.\nIf it was a legitimate message, it may still be delivered to the target recipient(s)."; + fake_response_text = US"Your message has been rejected but is being kept for evaluation.\nIf it was a legitimate message, it may still be delivered to the target recipient(s)."; } break; @@ -2148,7 +2503,7 @@ for (; cb != NULL; cb = cb->next) { uschar *pp = p + 6; while (*pp != 0 && *pp != '/') pp++; - originator_name = string_copy(parse_fix_phrase(p+6, pp-p-6, + submission_name = string_copy(parse_fix_phrase(p+6, pp-p-6, big_buffer, big_buffer_size)); p = pp; } @@ -2160,6 +2515,10 @@ for (; cb != NULL; cb = cb->next) return ERROR; } break; + + case CONTROL_SUPPRESS_LOCAL_FIXUPS: + suppress_local_fixups = TRUE; + break; } break; @@ -2408,6 +2767,10 @@ for (; cb != NULL; cb = cb->next) break; #endif + case ACLC_RATELIMIT: + rc = acl_ratelimit(arg, log_msgptr); + break; + case ACLC_RECIPIENTS: rc = match_address_list(addr->address, TRUE, TRUE, &arg, NULL, -1, 0, &recipient_data); @@ -2807,7 +3170,7 @@ if (Ustrchr(ss, ' ') == NULL) return ERROR; } acl_text[statbuf.st_size] = 0; - close(fd); + (void)close(fd); acl_name = string_sprintf("ACL \"%s\"", ss); HDEBUG(D_acl) debug_printf("read ACL from file %s\n", ss); @@ -2994,7 +3357,7 @@ acl_check_internal() to do the actual work. Arguments: where ACL_WHERE_xxxx indicating where called from - data_string RCPT address, or SMTP command argument, or NULL + recipient RCPT address for RCPT check, else NULL s the input string; NULL is the same as an empty ACL => DENY user_msgptr where to put a user error (for SMTP response) log_msgptr where to put a logging message (not for SMTP response) @@ -3008,21 +3371,22 @@ Returns: OK access is granted by an ACCEPT verb */ int -acl_check(int where, uschar *data_string, uschar *s, uschar **user_msgptr, +acl_check(int where, uschar *recipient, uschar *s, uschar **user_msgptr, uschar **log_msgptr) { int rc; address_item adb; -address_item *addr; +address_item *addr = NULL; *user_msgptr = *log_msgptr = NULL; sender_verified_failed = NULL; +ratelimiters_cmd = NULL; if (where == ACL_WHERE_RCPT) { adb = address_defaults; addr = &adb; - addr->address = data_string; + addr->address = recipient; if (deliver_split_address(addr) == DEFER) { *log_msgptr = US"defer in percent_hack_domains check"; @@ -3031,16 +3395,11 @@ if (where == ACL_WHERE_RCPT) deliver_domain = addr->domain; deliver_localpart = addr->local_part; } -else - { - addr = NULL; - smtp_command_argument = data_string; - } rc = acl_check_internal(where, addr, s, 0, user_msgptr, log_msgptr); -smtp_command_argument = deliver_domain = - deliver_localpart = deliver_address_data = sender_address_data = NULL; +deliver_domain = deliver_localpart = deliver_address_data = + sender_address_data = NULL; /* A DISCARD response is permitted only for message ACLs, excluding the PREDATA ACL, which is really in the middle of an SMTP command. */