static int perform_redis_search(const uschar *command, uschar *server, uschar **resultptr, uschar **errmsg, BOOL *defer_break, uint *do_cache) { redisContext *redis_handle = NULL; /* Keep compilers happy */ redisReply *redis_reply = NULL; redisReply *entry = NULL; redisReply *tentry = NULL; redis_connection *cn; int ssize = 0; int offset = 0; int yield = DEFER; int i, j; uschar *result = NULL; uschar *server_copy = NULL; uschar *tmp, *ttmp; uschar *sdata[3]; /* Disaggregate the parameters from the server argument. The order is host:port(socket) We can write to the string, since it is in a nextinlist temporary buffer. This copy is also used for debugging output. */ memset(sdata, 0, sizeof(sdata)) /* Set all to NULL */; for (i = 2; i > 0; i--) { uschar *pp = Ustrrchr(server, '/'); if (!pp) { *errmsg = string_sprintf("incomplete Redis server data: %s", i == 2 ? server : server_copy); *defer_break = TRUE; return DEFER; } *pp++ = 0; sdata[i] = pp; if (i == 2) server_copy = string_copy(server); /* sans password */ } sdata[0] = server; /* What's left at the start */ /* If the database or password is an empty string, set it NULL */ if (sdata[1][0] == 0) sdata[1] = NULL; if (sdata[2][0] == 0) sdata[2] = NULL; /* See if we have a cached connection to the server */ for (cn = redis_connections; cn; cn = cn->next) if (Ustrcmp(cn->server, server_copy) == 0) { redis_handle = cn->handle; break; } if (!cn) { uschar *p; uschar *socket = NULL; int port = 0; /* int redis_err = REDIS_OK; */ if ((p = Ustrchr(sdata[0], '('))) { *p++ = 0; socket = p; while (*p != 0 && *p != ')') p++; *p = 0; } if ((p = Ustrchr(sdata[0], ':'))) { *p++ = 0; port = Uatoi(p); } else port = Uatoi("6379"); if (Ustrchr(server, '/')) { *errmsg = string_sprintf("unexpected slash in Redis server hostname: %s", sdata[0]); *defer_break = TRUE; return DEFER; } DEBUG(D_lookup) debug_printf("REDIS new connection: host=%s port=%d socket=%s database=%s\n", sdata[0], port, socket, sdata[1]); /* Get store for a new handle, initialize it, and connect to the server */ /* XXX: Use timeouts ? */ redis_handle = socket ? redisConnectUnix(CCS socket) : redisConnect(CCS server, port); if (!redis_handle) { *errmsg = string_sprintf("REDIS connection failed"); *defer_break = FALSE; goto REDIS_EXIT; } /* Add the connection to the cache */ cn = store_get(sizeof(redis_connection)); cn->server = server_copy; cn->handle = redis_handle; cn->next = redis_connections; redis_connections = cn; } else { DEBUG(D_lookup) debug_printf("REDIS using cached connection for %s\n", server_copy); } /* Authenticate if there is a password */ if(sdata[2]) if (!(redis_reply = redisCommand(redis_handle, "AUTH %s", sdata[2]))) { *errmsg = string_sprintf("REDIS Authentication failed: %s\n", redis_handle->errstr); *defer_break = FALSE; goto REDIS_EXIT; } /* Select the database if there is a dbnumber passed */ if(sdata[1]) { if (!(redis_reply = redisCommand(redis_handle, "SELECT %s", sdata[1]))) { *errmsg = string_sprintf("REDIS: Selecting database=%s failed: %s\n", sdata[1], redis_handle->errstr); *defer_break = FALSE; goto REDIS_EXIT; } DEBUG(D_lookup) debug_printf("REDIS: Selecting database=%s\n", sdata[1]); } /* split string on whitespace into argv */ { uschar * argv[32]; int i; const uschar * s = command; int siz, ptr; uschar c; while (isspace(*s)) s++; for (i = 0; *s && i < nele(argv); i++) { for (argv[i] = NULL, siz = ptr = 0; (c = *s) && !isspace(c); s++) if (c != '\\' || *++s) /* backslash protects next char */ argv[i] = string_cat(argv[i], &siz, &ptr, s, 1); *(argv[i]+ptr) = '\0'; DEBUG(D_lookup) debug_printf("REDIS: argv[%d] '%s'\n", i, argv[i]); while (isspace(*s)) s++; } /* Run the command. We use the argv form rather than plain as that parses into args by whitespace yet has no escaping mechanism. */ redis_reply = redisCommandArgv(redis_handle, i, (const char **) argv, NULL); if (!redis_reply) { *errmsg = string_sprintf("REDIS: query failed: %s\n", redis_handle->errstr); *defer_break = FALSE; goto REDIS_EXIT; } } switch (redis_reply->type) { case REDIS_REPLY_ERROR: *errmsg = string_sprintf("REDIS: lookup result failed: %s\n", redis_reply->str); *defer_break = FALSE; *do_cache = 0; goto REDIS_EXIT; /* NOTREACHED */ case REDIS_REPLY_NIL: DEBUG(D_lookup) debug_printf("REDIS: query was not one that returned any data\n"); result = string_sprintf(""); *do_cache = 0; goto REDIS_EXIT; /* NOTREACHED */ case REDIS_REPLY_INTEGER: ttmp = (redis_reply->integer != 0) ? US"true" : US"false"; result = string_cat(result, &ssize, &offset, US ttmp, Ustrlen(ttmp)); break; case REDIS_REPLY_STRING: case REDIS_REPLY_STATUS: result = string_cat(result, &ssize, &offset, US redis_reply->str, redis_reply->len); break; case REDIS_REPLY_ARRAY: /* NOTE: For now support 1 nested array result. If needed a limitless result can be parsed */ for (i = 0; i < redis_reply->elements; i++) { entry = redis_reply->element[i]; if (result) result = string_cat(result, &ssize, &offset, US"\n", 1); switch (entry->type) { case REDIS_REPLY_INTEGER: tmp = string_sprintf("%d", entry->integer); result = string_cat(result, &ssize, &offset, US tmp, Ustrlen(tmp)); break; case REDIS_REPLY_STRING: result = string_cat(result, &ssize, &offset, US entry->str, entry->len); break; case REDIS_REPLY_ARRAY: for (j = 0; j < entry->elements; j++) { tentry = entry->element[j]; if (result) result = string_cat(result, &ssize, &offset, US"\n", 1); switch (tentry->type) { case REDIS_REPLY_INTEGER: ttmp = string_sprintf("%d", tentry->integer); result = string_cat(result, &ssize, &offset, US ttmp, Ustrlen(ttmp)); break; case REDIS_REPLY_STRING: result = string_cat(result, &ssize, &offset, US tentry->str, tentry->len); break; case REDIS_REPLY_ARRAY: DEBUG(D_lookup) debug_printf("REDIS: result has nesting of arrays which" " is not supported. Ignoring!\n"); break; default: DEBUG(D_lookup) debug_printf( "REDIS: result has unsupported type. Ignoring!\n"); break; } } break; default: DEBUG(D_lookup) debug_printf("REDIS: query returned unsupported type\n"); break; } } break; } if (result) { result[offset] = 0; store_reset(result + offset + 1); } else { yield = FAIL; *errmsg = US"REDIS: no data found"; } REDIS_EXIT: /* Free store for any result that was got; don't close the connection, as it is cached. */ if (redis_reply) freeReplyObject(redis_reply); /* Non-NULL result indicates a sucessful result */ if (result) { *resultptr = result; return OK; } else { DEBUG(D_lookup) debug_printf("%s\n", *errmsg); /* NOTE: Required to close connection since it needs to be reopened */ return yield; /* FAIL or DEFER */ } }
static int fakens_search(const uschar *domain, int type, uschar *answerptr, int size) { int len = Ustrlen(domain); int asize = size; /* Locally modified */ uschar *endname; uschar name[256]; uschar utilname[256]; uschar *aptr = answerptr; /* Locally modified */ struct stat statbuf; /* Remove terminating dot. */ if (domain[len - 1] == '.') len--; Ustrncpy(name, domain, len); name[len] = 0; endname = name + len; /* This code, for forcing TRY_AGAIN and NO_RECOVERY, is here so that it works for the old test suite that uses a real nameserver. When the old test suite is eventually abandoned, this code could be moved into the fakens utility. */ if (len >= 14 && Ustrcmp(endname - 14, "test.again.dns") == 0) { int delay = Uatoi(name); /* digits at the start of the name */ DEBUG(D_dns) debug_printf("Return from DNS lookup of %s (%s) faked for testing\n", name, dns_text_type(type)); if (delay > 0) { DEBUG(D_dns) debug_printf("delaying %d seconds\n", delay); sleep(delay); } h_errno = TRY_AGAIN; return -1; } if (len >= 13 && Ustrcmp(endname - 13, "test.fail.dns") == 0) { DEBUG(D_dns) debug_printf("Return from DNS lookup of %s (%s) faked for testing\n", name, dns_text_type(type)); h_errno = NO_RECOVERY; return -1; } /* Look for the fakens utility, and if it exists, call it. */ (void)string_format(utilname, sizeof(utilname), "%s/../bin/fakens", spool_directory); if (stat(CS utilname, &statbuf) >= 0) { pid_t pid; int infd, outfd, rc; uschar *argv[5]; DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) using fakens\n", name, dns_text_type(type)); argv[0] = utilname; argv[1] = spool_directory; argv[2] = name; argv[3] = dns_text_type(type); argv[4] = NULL; pid = child_open(argv, NULL, 0000, &infd, &outfd, FALSE); if (pid < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to run fakens: %s", strerror(errno)); len = 0; rc = -1; while (asize > 0 && (rc = read(outfd, aptr, asize)) > 0) { len += rc; aptr += rc; /* Don't modify the actual arguments, because they */ asize -= rc; /* may need to be passed on to res_search(). */ } if (rc < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "read from fakens failed: %s", strerror(errno)); switch(child_close(pid, 0)) { case 0: return len; case 1: h_errno = HOST_NOT_FOUND; return -1; case 2: h_errno = TRY_AGAIN; return -1; default: case 3: h_errno = NO_RECOVERY; return -1; case 4: h_errno = NO_DATA; return -1; case 5: /* Pass on to res_search() */ DEBUG(D_dns) debug_printf("fakens returned PASS_ON\n"); } } /* fakens utility not found, or it returned "pass on" */ DEBUG(D_dns) debug_printf("passing %s on to res_search()\n", domain); return res_search(CS domain, C_IN, type, answerptr, size); }
int spool_read_header(uschar *name, BOOL read_headers, BOOL subdir_set) { FILE *f = NULL; int n; int rcount = 0; long int uid, gid; BOOL inheader = FALSE; uschar *p; /* Reset all the global variables to their default values. However, there is one exception. DO NOT change the default value of dont_deliver, because it may be forced by an external setting. */ acl_var_c = acl_var_m = NULL; authenticated_id = NULL; authenticated_sender = NULL; allow_unqualified_recipient = FALSE; allow_unqualified_sender = FALSE; body_linecount = 0; body_zerocount = 0; deliver_firsttime = FALSE; deliver_freeze = FALSE; deliver_frozen_at = 0; deliver_manual_thaw = FALSE; /* dont_deliver must NOT be reset */ header_list = header_last = NULL; host_lookup_deferred = FALSE; host_lookup_failed = FALSE; interface_address = NULL; interface_port = 0; local_error_message = FALSE; local_scan_data = NULL; max_received_linelength = 0; message_linecount = 0; received_protocol = NULL; received_count = 0; recipients_list = NULL; sender_address = NULL; sender_fullhost = NULL; sender_helo_name = NULL; sender_host_address = NULL; sender_host_name = NULL; sender_host_port = 0; sender_host_authenticated = NULL; sender_ident = NULL; sender_local = FALSE; sender_set_untrusted = FALSE; smtp_active_hostname = primary_hostname; tree_nonrecipients = NULL; #ifdef EXPERIMENTAL_BRIGHTMAIL bmi_run = 0; bmi_verdicts = NULL; #endif #ifndef DISABLE_DKIM dkim_signers = NULL; dkim_disable_verify = FALSE; dkim_collect_input = FALSE; #endif #ifdef SUPPORT_TLS tls_in.certificate_verified = FALSE; # ifdef EXPERIMENTAL_DANE tls_in.dane_verified = FALSE; # endif tls_in.cipher = NULL; # ifndef COMPILE_UTILITY /* tls support fns not built in */ tls_free_cert(&tls_in.ourcert); tls_free_cert(&tls_in.peercert); # endif tls_in.peerdn = NULL; tls_in.sni = NULL; tls_in.ocsp = OCSP_NOT_REQ; #endif #ifdef WITH_CONTENT_SCAN spam_bar = NULL; spam_score = NULL; spam_score_int = NULL; #endif #if defined(SUPPORT_I18N) && !defined(COMPILE_UTILITY) message_smtputf8 = FALSE; message_utf8_downconvert = 0; #endif dsn_ret = 0; dsn_envid = NULL; /* Generate the full name and open the file. If message_subdir is already set, just look in the given directory. Otherwise, look in both the split and unsplit directories, as for the data file above. */ for (n = 0; n < 2; n++) { if (!subdir_set) message_subdir[0] = (split_spool_directory == (n == 0))? name[5] : 0; sprintf(CS big_buffer, "%s/input/%s/%s", spool_directory, message_subdir, name); f = Ufopen(big_buffer, "rb"); if (f != NULL) break; if (n != 0 || subdir_set || errno != ENOENT) return spool_read_notopen; } errno = 0; #ifndef COMPILE_UTILITY DEBUG(D_deliver) debug_printf("reading spool file %s\n", name); #endif /* COMPILE_UTILITY */ /* The first line of a spool file contains the message id followed by -H (i.e. the file name), in order to make the file self-identifying. */ if (Ufgets(big_buffer, big_buffer_size, f) == NULL) goto SPOOL_READ_ERROR; if (Ustrlen(big_buffer) != MESSAGE_ID_LENGTH + 3 || Ustrncmp(big_buffer, name, MESSAGE_ID_LENGTH + 2) != 0) goto SPOOL_FORMAT_ERROR; /* The next three lines in the header file are in a fixed format. The first contains the login, uid, and gid of the user who caused the file to be written. There are known cases where a negative gid is used, so we allow for both negative uids and gids. The second contains the mail address of the message's sender, enclosed in <>. The third contains the time the message was received, and the number of warning messages for delivery delays that have been sent. */ if (Ufgets(big_buffer, big_buffer_size, f) == NULL) goto SPOOL_READ_ERROR; p = big_buffer + Ustrlen(big_buffer); while (p > big_buffer && isspace(p[-1])) p--; *p = 0; if (!isdigit(p[-1])) goto SPOOL_FORMAT_ERROR; while (p > big_buffer && (isdigit(p[-1]) || '-' == p[-1])) p--; gid = Uatoi(p); if (p <= big_buffer || *(--p) != ' ') goto SPOOL_FORMAT_ERROR; *p = 0; if (!isdigit(p[-1])) goto SPOOL_FORMAT_ERROR; while (p > big_buffer && (isdigit(p[-1]) || '-' == p[-1])) p--; uid = Uatoi(p); if (p <= big_buffer || *(--p) != ' ') goto SPOOL_FORMAT_ERROR; *p = 0; originator_login = string_copy(big_buffer); originator_uid = (uid_t)uid; originator_gid = (gid_t)gid; /* envelope from */ if (Ufgets(big_buffer, big_buffer_size, f) == NULL) goto SPOOL_READ_ERROR; n = Ustrlen(big_buffer); if (n < 3 || big_buffer[0] != '<' || big_buffer[n-2] != '>') goto SPOOL_FORMAT_ERROR; sender_address = store_get(n-2); Ustrncpy(sender_address, big_buffer+1, n-3); sender_address[n-3] = 0; /* time */ if (Ufgets(big_buffer, big_buffer_size, f) == NULL) goto SPOOL_READ_ERROR; if (sscanf(CS big_buffer, "%d %d", &received_time, &warning_count) != 2) goto SPOOL_FORMAT_ERROR; message_age = time(NULL) - received_time; #ifndef COMPILE_UTILITY DEBUG(D_deliver) debug_printf("user=%s uid=%ld gid=%ld sender=%s\n", originator_login, (long int)originator_uid, (long int)originator_gid, sender_address); #endif /* COMPILE_UTILITY */ /* Now there may be a number of optional lines, each starting with "-". If you add a new setting here, make sure you set the default above. Because there are now quite a number of different possibilities, we use a switch on the first character to avoid too many failing tests. Thanks to Nico Erfurth for the patch that implemented this. I have made it even more efficient by not re-scanning the first two characters. To allow new versions of Exim that add additional flags to interwork with older versions that do not understand them, just ignore any lines starting with "-" that we don't recognize. Otherwise it wouldn't be possible to back off a new version that left new-style flags written on the spool. */ p = big_buffer + 2; for (;;) { int len; if (Ufgets(big_buffer, big_buffer_size, f) == NULL) goto SPOOL_READ_ERROR; if (big_buffer[0] != '-') break; while ( (len = Ustrlen(big_buffer)) == big_buffer_size-1 && big_buffer[len-1] != '\n' ) { /* buffer not big enough for line; certs make this possible */ uschar * buf; if (big_buffer_size >= BIG_BUFFER_SIZE*4) goto SPOOL_READ_ERROR; buf = store_get_perm(big_buffer_size *= 2); memcpy(buf, big_buffer, --len); big_buffer = buf; if (Ufgets(big_buffer+len, big_buffer_size-len, f) == NULL) goto SPOOL_READ_ERROR; } big_buffer[len-1] = 0; switch(big_buffer[1]) { case 'a': /* Nowadays we use "-aclc" and "-aclm" for the different types of ACL variable, because Exim allows any number of them, with arbitrary names. The line in the spool file is "-acl[cm] <name> <length>". The name excludes the c or m. */ if (Ustrncmp(p, "clc ", 4) == 0 || Ustrncmp(p, "clm ", 4) == 0) { uschar *name, *endptr; int count; tree_node *node; endptr = Ustrchr(big_buffer + 6, ' '); if (endptr == NULL) goto SPOOL_FORMAT_ERROR; name = string_sprintf("%c%.*s", big_buffer[4], endptr - big_buffer - 6, big_buffer + 6); if (sscanf(CS endptr, " %d", &count) != 1) goto SPOOL_FORMAT_ERROR; node = acl_var_create(name); node->data.ptr = store_get(count + 1); if (fread(node->data.ptr, 1, count+1, f) < count) goto SPOOL_READ_ERROR; ((uschar*)node->data.ptr)[count] = 0; } else if (Ustrcmp(p, "llow_unqualified_recipient") == 0) allow_unqualified_recipient = TRUE; else if (Ustrcmp(p, "llow_unqualified_sender") == 0) allow_unqualified_sender = TRUE; else if (Ustrncmp(p, "uth_id", 6) == 0) authenticated_id = string_copy(big_buffer + 9); else if (Ustrncmp(p, "uth_sender", 10) == 0) authenticated_sender = string_copy(big_buffer + 13); else if (Ustrncmp(p, "ctive_hostname", 14) == 0) smtp_active_hostname = string_copy(big_buffer + 17); /* For long-term backward compatibility, we recognize "-acl", which was used before the number of ACL variables changed from 10 to 20. This was before the subsequent change to an arbitrary number of named variables. This code is retained so that upgrades from very old versions can still handle old-format spool files. The value given after "-acl" is a number that is 0-9 for connection variables, and 10-19 for message variables. */ else if (Ustrncmp(p, "cl ", 3) == 0) { int index, count; uschar name[20]; /* Need plenty of space for %d format */ tree_node *node; if ( sscanf(CS big_buffer + 5, "%d %d", &index, &count) != 2 || index >= 20 ) goto SPOOL_FORMAT_ERROR; if (index < 10) (void) string_format(name, sizeof(name), "%c%d", 'c', index); else (void) string_format(name, sizeof(name), "%c%d", 'm', index - 10); node = acl_var_create(name); node->data.ptr = store_get(count + 1); if (fread(node->data.ptr, 1, count+1, f) < count) goto SPOOL_READ_ERROR; ((uschar*)node->data.ptr)[count] = 0; } break; case 'b': if (Ustrncmp(p, "ody_linecount", 13) == 0) body_linecount = Uatoi(big_buffer + 15); else if (Ustrncmp(p, "ody_zerocount", 13) == 0) body_zerocount = Uatoi(big_buffer + 15); #ifdef EXPERIMENTAL_BRIGHTMAIL else if (Ustrncmp(p, "mi_verdicts ", 12) == 0) bmi_verdicts = string_copy(big_buffer + 14); #endif break; case 'd': if (Ustrcmp(p, "eliver_firsttime") == 0) deliver_firsttime = TRUE; /* Check if the dsn flags have been set in the header file */ else if (Ustrncmp(p, "sn_ret", 6) == 0) dsn_ret= atoi(CS big_buffer + 8); else if (Ustrncmp(p, "sn_envid", 8) == 0) dsn_envid = string_copy(big_buffer + 11); break; case 'f': if (Ustrncmp(p, "rozen", 5) == 0) { deliver_freeze = TRUE; if (sscanf(CS big_buffer+7, TIME_T_FMT, &deliver_frozen_at) != 1) goto SPOOL_READ_ERROR; } break; case 'h': if (Ustrcmp(p, "ost_lookup_deferred") == 0) host_lookup_deferred = TRUE; else if (Ustrcmp(p, "ost_lookup_failed") == 0) host_lookup_failed = TRUE; else if (Ustrncmp(p, "ost_auth", 8) == 0) sender_host_authenticated = string_copy(big_buffer + 11); else if (Ustrncmp(p, "ost_name", 8) == 0) sender_host_name = string_copy(big_buffer + 11); else if (Ustrncmp(p, "elo_name", 8) == 0) sender_helo_name = string_copy(big_buffer + 11); /* We now record the port number after the address, separated by a dot. For compatibility during upgrading, do nothing if there isn't a value (it gets left at zero). */ else if (Ustrncmp(p, "ost_address", 11) == 0) { sender_host_port = host_address_extract_port(big_buffer + 14); sender_host_address = string_copy(big_buffer + 14); } break; case 'i': if (Ustrncmp(p, "nterface_address", 16) == 0) { interface_port = host_address_extract_port(big_buffer + 19); interface_address = string_copy(big_buffer + 19); } else if (Ustrncmp(p, "dent", 4) == 0) sender_ident = string_copy(big_buffer + 7); break; case 'l': if (Ustrcmp(p, "ocal") == 0) sender_local = TRUE; else if (Ustrcmp(big_buffer, "-localerror") == 0) local_error_message = TRUE; else if (Ustrncmp(p, "ocal_scan ", 10) == 0) local_scan_data = string_copy(big_buffer + 12); break; case 'm': if (Ustrcmp(p, "anual_thaw") == 0) deliver_manual_thaw = TRUE; else if (Ustrncmp(p, "ax_received_linelength", 22) == 0) max_received_linelength = Uatoi(big_buffer + 24); break; case 'N': if (*p == 0) dont_deliver = TRUE; /* -N */ break; case 'r': if (Ustrncmp(p, "eceived_protocol", 16) == 0) received_protocol = string_copy(big_buffer + 19); break; case 's': if (Ustrncmp(p, "ender_set_untrusted", 19) == 0) sender_set_untrusted = TRUE; #ifdef WITH_CONTENT_SCAN else if (Ustrncmp(p, "pam_bar ", 8) == 0) spam_bar = string_copy(big_buffer + 10); else if (Ustrncmp(p, "pam_score ", 10) == 0) spam_score = string_copy(big_buffer + 12); else if (Ustrncmp(p, "pam_score_int ", 14) == 0) spam_score_int = string_copy(big_buffer + 16); #endif #if defined(SUPPORT_I18N) && !defined(COMPILE_UTILITY) else if (Ustrncmp(p, "mtputf8", 7) == 0) message_smtputf8 = TRUE; #endif break; #ifdef SUPPORT_TLS case 't': if (Ustrncmp(p, "ls_certificate_verified", 23) == 0) tls_in.certificate_verified = TRUE; else if (Ustrncmp(p, "ls_cipher", 9) == 0) tls_in.cipher = string_copy(big_buffer + 12); # ifndef COMPILE_UTILITY /* tls support fns not built in */ else if (Ustrncmp(p, "ls_ourcert", 10) == 0) (void) tls_import_cert(big_buffer + 13, &tls_in.ourcert); else if (Ustrncmp(p, "ls_peercert", 11) == 0) (void) tls_import_cert(big_buffer + 14, &tls_in.peercert); # endif else if (Ustrncmp(p, "ls_peerdn", 9) == 0) tls_in.peerdn = string_unprinting(string_copy(big_buffer + 12)); else if (Ustrncmp(p, "ls_sni", 6) == 0) tls_in.sni = string_unprinting(string_copy(big_buffer + 9)); else if (Ustrncmp(p, "ls_ocsp", 7) == 0) tls_in.ocsp = big_buffer[10] - '0'; break; #endif #if defined(SUPPORT_I18N) && !defined(COMPILE_UTILITY) case 'u': if (Ustrncmp(p, "tf8_downcvt", 11) == 0) message_utf8_downconvert = 1; else if (Ustrncmp(p, "tf8_optdowncvt", 15) == 0) message_utf8_downconvert = -1; break; #endif default: /* Present because some compilers complain if all */ break; /* possibilities are not covered. */ } } /* Build sender_fullhost if required */ #ifndef COMPILE_UTILITY host_build_sender_fullhost(); #endif /* COMPILE_UTILITY */ #ifndef COMPILE_UTILITY DEBUG(D_deliver) debug_printf("sender_local=%d ident=%s\n", sender_local, (sender_ident == NULL)? US"unset" : sender_ident); #endif /* COMPILE_UTILITY */ /* We now have the tree of addresses NOT to deliver to, or a line containing "XX", indicating no tree. */ if (Ustrncmp(big_buffer, "XX\n", 3) != 0 && !read_nonrecipients_tree(&tree_nonrecipients, f, big_buffer, big_buffer_size)) goto SPOOL_FORMAT_ERROR; #ifndef COMPILE_UTILITY DEBUG(D_deliver) { debug_printf("Non-recipients:\n"); debug_print_tree(tree_nonrecipients); } #endif /* COMPILE_UTILITY */ /* After reading the tree, the next line has not yet been read into the buffer. It contains the count of recipients which follow on separate lines. */ if (Ufgets(big_buffer, big_buffer_size, f) == NULL) goto SPOOL_READ_ERROR; if (sscanf(CS big_buffer, "%d", &rcount) != 1) goto SPOOL_FORMAT_ERROR; #ifndef COMPILE_UTILITY DEBUG(D_deliver) debug_printf("recipients_count=%d\n", rcount); #endif /* COMPILE_UTILITY */ recipients_list_max = rcount; recipients_list = store_get(rcount * sizeof(recipient_item)); for (recipients_count = 0; recipients_count < rcount; recipients_count++) { int nn; int pno = -1; int dsn_flags = 0; uschar *orcpt = NULL; uschar *errors_to = NULL; uschar *p; if (Ufgets(big_buffer, big_buffer_size, f) == NULL) goto SPOOL_READ_ERROR; nn = Ustrlen(big_buffer); if (nn < 2) goto SPOOL_FORMAT_ERROR; /* Remove the newline; this terminates the address if there is no additional data on the line. */ p = big_buffer + nn - 1; *p-- = 0; /* Look back from the end of the line for digits and special terminators. Since an address must end with a domain, we can tell that extra data is present by the presence of the terminator, which is always some character that cannot exist in a domain. (If I'd thought of the need for additional data early on, I'd have put it at the start, with the address at the end. As it is, we have to operate backwards. Addresses are permitted to contain spaces, you see.) This code has to cope with various versions of this data that have evolved over time. In all cases, the line might just contain an address, with no additional data. Otherwise, the possibilities are as follows: Exim 3 type: <address><space><digits>,<digits>,<digits> The second set of digits is the parent number for one_time addresses. The other values were remnants of earlier experiments that were abandoned. Exim 4 first type: <address><space><digits> The digits are the parent number for one_time addresses. Exim 4 new type: <address><space><data>#<type bits> The type bits indicate what the contents of the data are. Bit 01 indicates that, reading from right to left, the data ends with <errors_to address><space><len>,<pno> where pno is the parent number for one_time addresses, and len is the length of the errors_to address (zero meaning none). Bit 02 indicates that, again reading from right to left, the data continues with orcpt len(orcpt),dsn_flags */ while (isdigit(*p)) p--; /* Handle Exim 3 spool files */ if (*p == ',') { int dummy; while (isdigit(*(--p)) || *p == ','); if (*p == ' ') { *p++ = 0; (void)sscanf(CS p, "%d,%d", &dummy, &pno); } } /* Handle early Exim 4 spool files */ else if (*p == ' ') { *p++ = 0; (void)sscanf(CS p, "%d", &pno); } /* Handle current format Exim 4 spool files */ else if (*p == '#') { int flags; #if !defined (COMPILE_UTILITY) DEBUG(D_deliver) debug_printf("**** SPOOL_IN - Exim 4 standard format spoolfile\n"); #endif (void)sscanf(CS p+1, "%d", &flags); if ((flags & 0x01) != 0) /* one_time data exists */ { int len; while (isdigit(*(--p)) || *p == ',' || *p == '-'); (void)sscanf(CS p+1, "%d,%d", &len, &pno); *p = 0; if (len > 0) { p -= len; errors_to = string_copy(p); } } *(--p) = 0; /* Terminate address */ if ((flags & 0x02) != 0) /* one_time data exists */ { int len; while (isdigit(*(--p)) || *p == ',' || *p == '-'); (void)sscanf(CS p+1, "%d,%d", &len, &dsn_flags); *p = 0; if (len > 0) { p -= len; orcpt = string_copy(p); } } *(--p) = 0; /* Terminate address */ } #if !defined(COMPILE_UTILITY) else { DEBUG(D_deliver) debug_printf("**** SPOOL_IN - No additional fields\n"); } if ((orcpt != NULL) || (dsn_flags != 0)) { DEBUG(D_deliver) debug_printf("**** SPOOL_IN - address: |%s| orcpt: |%s| dsn_flags: %d\n", big_buffer, orcpt, dsn_flags); } if (errors_to != NULL) { DEBUG(D_deliver) debug_printf("**** SPOOL_IN - address: |%s| errorsto: |%s|\n", big_buffer, errors_to); } #endif recipients_list[recipients_count].address = string_copy(big_buffer); recipients_list[recipients_count].pno = pno; recipients_list[recipients_count].errors_to = errors_to; recipients_list[recipients_count].orcpt = orcpt; recipients_list[recipients_count].dsn_flags = dsn_flags; } /* The remainder of the spool header file contains the headers for the message, separated off from the previous data by a blank line. Each header is preceded by a count of its length and either a certain letter (for various identified headers), space (for a miscellaneous live header) or an asterisk (for a header that has been rewritten). Count the Received: headers. We read the headers always, in order to check on the format of the file, but only create a header list if requested to do so. */ inheader = TRUE; if (Ufgets(big_buffer, big_buffer_size, f) == NULL) goto SPOOL_READ_ERROR; if (big_buffer[0] != '\n') goto SPOOL_FORMAT_ERROR; while ((n = fgetc(f)) != EOF) { header_line *h; uschar flag[4]; int i; if (!isdigit(n)) goto SPOOL_FORMAT_ERROR; if(ungetc(n, f) == EOF || fscanf(f, "%d%c ", &n, flag) == EOF) goto SPOOL_READ_ERROR; if (flag[0] != '*') message_size += n; /* Omit non-transmitted headers */ if (read_headers) { h = store_get(sizeof(header_line)); h->next = NULL; h->type = flag[0]; h->slen = n; h->text = store_get(n+1); if (h->type == htype_received) received_count++; if (header_list == NULL) header_list = h; else header_last->next = h; header_last = h; for (i = 0; i < n; i++) { int c = fgetc(f); if (c == 0 || c == EOF) goto SPOOL_FORMAT_ERROR; if (c == '\n' && h->type != htype_old) message_linecount++; h->text[i] = c; } h->text[i] = 0; } /* Not requiring header data, just skip through the bytes */ else for (i = 0; i < n; i++) { int c = fgetc(f); if (c == 0 || c == EOF) goto SPOOL_FORMAT_ERROR; } } /* We have successfully read the data in the header file. Update the message line count by adding the body linecount to the header linecount. Close the file and give a positive response. */ #ifndef COMPILE_UTILITY DEBUG(D_deliver) debug_printf("body_linecount=%d message_linecount=%d\n", body_linecount, message_linecount); #endif /* COMPILE_UTILITY */ message_linecount += body_linecount; fclose(f); return spool_read_OK; /* There was an error reading the spool or there was missing data, or there was a format error. A "read error" with no errno means an unexpected EOF, which we treat as a format error. */ SPOOL_READ_ERROR: if (errno != 0) { n = errno; #ifndef COMPILE_UTILITY DEBUG(D_any) debug_printf("Error while reading spool file %s\n", name); #endif /* COMPILE_UTILITY */ fclose(f); errno = n; return inheader? spool_read_hdrerror : spool_read_enverror; } SPOOL_FORMAT_ERROR: #ifndef COMPILE_UTILITY DEBUG(D_any) debug_printf("Format error in spool file %s\n", name); #endif /* COMPILE_UTILITY */ fclose(f); errno = ERRNO_SPOOLFORMAT; return inheader? spool_read_hdrerror : spool_read_enverror; }
BOOL pipe_transport_entry( transport_instance *tblock, /* data for this instantiation */ address_item *addr) /* address(es) we are working on */ { pid_t pid, outpid; int fd_in, fd_out, rc; int envcount = 0; int envsep = 0; int expand_fail; pipe_transport_options_block *ob = (pipe_transport_options_block *)(tblock->options_block); int timeout = ob->timeout; BOOL written_ok = FALSE; BOOL expand_arguments; const uschar **argv; uschar *envp[50]; const uschar *envlist = ob->environment; uschar *cmd, *ss; uschar *eol = (ob->use_crlf)? US"\r\n" : US"\n"; DEBUG(D_transport) debug_printf("%s transport entered\n", tblock->name); /* Set up for the good case */ addr->transport_return = OK; addr->basic_errno = 0; /* Pipes are not accepted as general addresses, but they can be generated from .forward files or alias files. In those cases, the pfr flag is set, and the command to be obeyed is pointed to by addr->local_part; it starts with the pipe symbol. In other cases, the command is supplied as one of the pipe transport's options. */ if (testflag(addr, af_pfr) && addr->local_part[0] == '|') { if (ob->force_command) { /* Enables expansion of $address_pipe into seperate arguments */ setflag(addr, af_force_command); cmd = ob->cmd; expand_arguments = TRUE; expand_fail = PANIC; } else { cmd = addr->local_part + 1; while (isspace(*cmd)) cmd++; expand_arguments = testflag(addr, af_expand_pipe); expand_fail = FAIL; } } else { cmd = ob->cmd; expand_arguments = TRUE; expand_fail = PANIC; } /* If no command has been supplied, we are in trouble. * We also check for an empty string since it may be * coming from addr->local_part[0] == '|' */ if (cmd == NULL || *cmd == '\0') { addr->transport_return = DEFER; addr->message = string_sprintf("no command specified for %s transport", tblock->name); return FALSE; } /* When a pipe is set up by a filter file, there may be values for $thisaddress and numerical the variables in existence. These are passed in addr->pipe_expandn for use here. */ if (expand_arguments && addr->pipe_expandn != NULL) { uschar **ss = addr->pipe_expandn; expand_nmax = -1; if (*ss != NULL) filter_thisaddress = *ss++; while (*ss != NULL) { expand_nstring[++expand_nmax] = *ss; expand_nlength[expand_nmax] = Ustrlen(*ss++); } } /* The default way of processing the command is to split it up into arguments here, and run it directly. This offers some security advantages. However, there are installations that want by default to run commands under /bin/sh always, so there is an option to do that. */ if (ob->use_shell) { if (!set_up_shell_command(&argv, cmd, expand_arguments, expand_fail, addr, tblock->name)) return FALSE; } else if (!set_up_direct_command(&argv, cmd, expand_arguments, expand_fail, addr, tblock->name, ob)) return FALSE; expand_nmax = -1; /* Reset */ filter_thisaddress = NULL; /* Set up the environment for the command. */ envp[envcount++] = string_sprintf("LOCAL_PART=%s", deliver_localpart); envp[envcount++] = string_sprintf("LOGNAME=%s", deliver_localpart); envp[envcount++] = string_sprintf("USER=%s", deliver_localpart); envp[envcount++] = string_sprintf("LOCAL_PART_PREFIX=%#s", deliver_localpart_prefix); envp[envcount++] = string_sprintf("LOCAL_PART_SUFFIX=%#s", deliver_localpart_suffix); envp[envcount++] = string_sprintf("DOMAIN=%s", deliver_domain); envp[envcount++] = string_sprintf("HOME=%#s", deliver_home); envp[envcount++] = string_sprintf("MESSAGE_ID=%s", message_id); envp[envcount++] = string_sprintf("PATH=%s", ob->path); envp[envcount++] = string_sprintf("RECIPIENT=%#s%#s%#s@%#s", deliver_localpart_prefix, deliver_localpart, deliver_localpart_suffix, deliver_domain); envp[envcount++] = string_sprintf("QUALIFY_DOMAIN=%s", qualify_domain_sender); envp[envcount++] = string_sprintf("SENDER=%s", sender_address); envp[envcount++] = US"SHELL=/bin/sh"; if (addr->host_list != NULL) envp[envcount++] = string_sprintf("HOST=%s", addr->host_list->name); if (timestamps_utc) envp[envcount++] = US"TZ=UTC"; else if (timezone_string != NULL && timezone_string[0] != 0) envp[envcount++] = string_sprintf("TZ=%s", timezone_string); /* Add any requested items */ if (envlist) { envlist = expand_cstring(envlist); if (envlist == NULL) { addr->transport_return = DEFER; addr->message = string_sprintf("failed to expand string \"%s\" " "for %s transport: %s", ob->environment, tblock->name, expand_string_message); return FALSE; } } while ((ss = string_nextinlist(&envlist, &envsep, big_buffer, big_buffer_size)) != NULL) { if (envcount > sizeof(envp)/sizeof(uschar *) - 2) { addr->transport_return = DEFER; addr->message = string_sprintf("too many environment settings for " "%s transport", tblock->name); return FALSE; } envp[envcount++] = string_copy(ss); } envp[envcount] = NULL; /* If the -N option is set, can't do any more. */ if (dont_deliver) { DEBUG(D_transport) debug_printf("*** delivery by %s transport bypassed by -N option", tblock->name); return FALSE; } /* Handling the output from the pipe is tricky. If a file for catching this output is provided, we could in theory just hand that fd over to the process, but this isn't very safe because it might loop and carry on writing for ever (which is exactly what happened in early versions of Exim). Therefore we use the standard child_open() function, which creates pipes. We can then read our end of the output pipe and count the number of bytes that come through, chopping the sub-process if it exceeds some limit. However, this means we want to run a sub-process with both its input and output attached to pipes. We can't handle that easily from a single parent process using straightforward code such as the transport_write_message() function because the subprocess might not be reading its input because it is trying to write to a full output pipe. The complication of redesigning the world to handle this is too great - simpler just to run another process to do the reading of the output pipe. */ /* As this is a local transport, we are already running with the required uid/gid and current directory. Request that the new process be a process group leader, so we can kill it and all its children on a timeout. */ if ((pid = child_open(USS argv, envp, ob->umask, &fd_in, &fd_out, TRUE)) < 0) { addr->transport_return = DEFER; addr->message = string_sprintf( "Failed to create child process for %s transport: %s", tblock->name, strerror(errno)); return FALSE; } /* Now fork a process to handle the output that comes down the pipe. */ if ((outpid = fork()) < 0) { addr->basic_errno = errno; addr->transport_return = DEFER; addr->message = string_sprintf( "Failed to create process for handling output in %s transport", tblock->name); (void)close(fd_in); (void)close(fd_out); return FALSE; } /* This is the code for the output-handling subprocess. Read from the pipe in chunks, and write to the return file if one is provided. Keep track of the number of bytes handled. If the limit is exceeded, try to kill the subprocess group, and in any case close the pipe and exit, which should cause the subprocess to fail. */ if (outpid == 0) { int count = 0; (void)close(fd_in); set_process_info("reading output from |%s", cmd); while ((rc = read(fd_out, big_buffer, big_buffer_size)) > 0) { if (addr->return_file >= 0) if(write(addr->return_file, big_buffer, rc) != rc) DEBUG(D_transport) debug_printf("Problem writing to return_file\n"); count += rc; if (count > ob->max_output) { DEBUG(D_transport) debug_printf("Too much output from pipe - killed\n"); if (addr->return_file >= 0) { uschar *message = US"\n\n*** Too much output - remainder discarded ***\n"; rc = Ustrlen(message); if(write(addr->return_file, message, rc) != rc) DEBUG(D_transport) debug_printf("Problem writing to return_file\n"); } killpg(pid, SIGKILL); break; } } (void)close(fd_out); _exit(0); } (void)close(fd_out); /* Not used in this process */ /* Carrying on now with the main parent process. Attempt to write the message to it down the pipe. It is a fallacy to think that you can detect write errors when the sub-process fails to read the pipe. The parent process may complete writing and close the pipe before the sub-process completes. We could sleep a bit here to let the sub-process get going, but it may still not complete. So we ignore all writing errors. (When in the test harness, we do do a short sleep so any debugging output is likely to be in the same order.) */ if (running_in_test_harness) millisleep(500); DEBUG(D_transport) debug_printf("Writing message to pipe\n"); /* Arrange to time out writes if there is a timeout set. */ if (timeout > 0) { sigalrm_seen = FALSE; transport_write_timeout = timeout; } /* Reset the counter of bytes written */ transport_count = 0; /* First write any configured prefix information */ if (ob->message_prefix != NULL) { uschar *prefix = expand_string(ob->message_prefix); if (prefix == NULL) { addr->transport_return = search_find_defer? DEFER : PANIC; addr->message = string_sprintf("Expansion of \"%s\" (prefix for %s " "transport) failed: %s", ob->message_prefix, tblock->name, expand_string_message); return FALSE; } if (!transport_write_block(fd_in, prefix, Ustrlen(prefix))) goto END_WRITE; } /* If the use_bsmtp option is set, we need to write SMTP prefix information. The various different values for batching are handled outside; if there is more than one address available here, all must be included. Force SMTP dot-handling. */ if (ob->use_bsmtp) { address_item *a; if (!transport_write_string(fd_in, "MAIL FROM:<%s>%s", return_path, eol)) goto END_WRITE; for (a = addr; a != NULL; a = a->next) { if (!transport_write_string(fd_in, "RCPT TO:<%s>%s", transport_rcpt_address(a, tblock->rcpt_include_affixes), eol)) goto END_WRITE; } if (!transport_write_string(fd_in, "DATA%s", eol)) goto END_WRITE; } /* Now the actual message - the options were set at initialization time */ if (!transport_write_message(addr, fd_in, ob->options, 0, tblock->add_headers, tblock->remove_headers, ob->check_string, ob->escape_string, tblock->rewrite_rules, tblock->rewrite_existflags)) goto END_WRITE; /* Now any configured suffix */ if (ob->message_suffix != NULL) { uschar *suffix = expand_string(ob->message_suffix); if (suffix == NULL) { addr->transport_return = search_find_defer? DEFER : PANIC; addr->message = string_sprintf("Expansion of \"%s\" (suffix for %s " "transport) failed: %s", ob->message_suffix, tblock->name, expand_string_message); return FALSE; } if (!transport_write_block(fd_in, suffix, Ustrlen(suffix))) goto END_WRITE; } /* If local_smtp, write the terminating dot. */ if (ob->use_bsmtp && !transport_write_string(fd_in, ".%s", eol)) goto END_WRITE; /* Flag all writing completed successfully. */ written_ok = TRUE; /* Come here if there are errors during writing. */ END_WRITE: /* OK, the writing is now all done. Close the pipe. */ (void) close(fd_in); /* Handle errors during writing. For timeouts, set the timeout for waiting for the child process to 1 second. If the process at the far end of the pipe died without reading all of it, we expect an EPIPE error, which should be ignored. We used also to ignore WRITEINCOMPLETE but the writing function is now cleverer at handling OS where the death of a pipe doesn't give EPIPE immediately. See comments therein. */ if (!written_ok) { if (errno == ETIMEDOUT) { addr->message = string_sprintf("%stimeout while writing to pipe", transport_filter_timed_out? "transport filter " : ""); addr->transport_return = ob->timeout_defer? DEFER : FAIL; timeout = 1; } else if (errno == EPIPE) { debug_printf("transport error EPIPE ignored\n"); } else { addr->transport_return = PANIC; addr->basic_errno = errno; if (errno == ERRNO_CHHEADER_FAIL) addr->message = string_sprintf("Failed to expand headers_add or headers_remove: %s", expand_string_message); else if (errno == ERRNO_FILTER_FAIL) addr->message = string_sprintf("Transport filter process failed (%d)%s", addr->more_errno, (addr->more_errno == EX_EXECFAILED)? ": unable to execute command" : ""); else if (errno == ERRNO_WRITEINCOMPLETE) addr->message = string_sprintf("Failed repeatedly to write data"); else addr->message = string_sprintf("Error %d", errno); return FALSE; } } /* Wait for the child process to complete and take action if the returned status is nonzero. The timeout will be just 1 second if any of the writes above timed out. */ if ((rc = child_close(pid, timeout)) != 0) { uschar *tmsg = (addr->message == NULL)? US"" : string_sprintf(" (preceded by %s)", addr->message); /* The process did not complete in time; kill its process group and fail the delivery. It appears to be necessary to kill the output process too, as otherwise it hangs on for some time if the actual pipe process is sleeping. (At least, that's what I observed on Solaris 2.5.1.) Since we are failing the delivery, that shouldn't cause any problem. */ if (rc == -256) { killpg(pid, SIGKILL); kill(outpid, SIGKILL); addr->transport_return = ob->timeout_defer? DEFER : FAIL; addr->message = string_sprintf("pipe delivery process timed out%s", tmsg); } /* Wait() failed. */ else if (rc == -257) { addr->transport_return = PANIC; addr->message = string_sprintf("Wait() failed for child process of %s " "transport: %s%s", tblock->name, strerror(errno), tmsg); } /* Since the transport_filter timed out we assume it has sent the child process a malformed or incomplete data stream. Kill off the child process and prevent checking its exit status as it will has probably exited in error. This prevents the transport_filter timeout message from getting overwritten by the exit error which is not the cause of the problem. */ else if (transport_filter_timed_out) { killpg(pid, SIGKILL); kill(outpid, SIGKILL); } /* Either the process completed, but yielded a non-zero (necessarily positive) status, or the process was terminated by a signal (rc will contain the negation of the signal number). Treat killing by signal as failure unless status is being ignored. By default, the message is bounced back, unless freeze_signal is set, in which case it is frozen instead. */ else if (rc < 0) { if (ob->freeze_signal) { addr->transport_return = DEFER; addr->special_action = SPECIAL_FREEZE; addr->message = string_sprintf("Child process of %s transport (running " "command \"%s\") was terminated by signal %d (%s)%s", tblock->name, cmd, -rc, os_strsignal(-rc), tmsg); } else if (!ob->ignore_status) { addr->transport_return = FAIL; addr->message = string_sprintf("Child process of %s transport (running " "command \"%s\") was terminated by signal %d (%s)%s", tblock->name, cmd, -rc, os_strsignal(-rc), tmsg); } } /* For positive values (process terminated with non-zero status), we need a status code to request deferral. A number of systems contain the following line in sysexits.h: #define EX_TEMPFAIL 75 with the description EX_TEMPFAIL -- temporary failure, indicating something that is not really an error. In sendmail, this means that a mailer (e.g.) could not create a connection, and the request should be reattempted later. Based on this, we use exit code EX_TEMPFAIL as a default to mean "defer" when not ignoring the returned status. However, there is now an option that contains a list of temporary codes, with TEMPFAIL and CANTCREAT as defaults. Another case that needs special treatment is if execve() failed (typically the command that was given is a non-existent path). By default this is treated as just another failure, but if freeze_exec_fail is set, the reaction is to freeze the message rather than bounce the address. Exim used to signal this failure with EX_UNAVAILABLE, which is definined in many systems as #define EX_UNAVAILABLE 69 with the description EX_UNAVAILABLE -- A service is unavailable. This can occur if a support program or file does not exist. This can also be used as a catchall message when something you wanted to do doesn't work, but you don't know why. However, this can be confused with a command that actually returns 69 because something *it* wanted is unavailable. At release 4.21, Exim was changed to use return code 127 instead, because this is what the shell returns when it is unable to exec a command. We define it as EX_EXECFAILED, and use it in child.c to signal execve() failure and other unexpected failures such as setuid() not working - though that won't be the case here because we aren't changing uid. */ else { /* Always handle execve() failure specially if requested to */ if (ob->freeze_exec_fail && (rc == EX_EXECFAILED)) { addr->transport_return = DEFER; addr->special_action = SPECIAL_FREEZE; addr->message = string_sprintf("pipe process failed to exec \"%s\"%s", cmd, tmsg); } /* Otherwise take action only if not ignoring status */ else if (!ob->ignore_status) { uschar *ss; int size, ptr, i; /* If temp_errors is "*" all codes are temporary. Initializion checks that it's either "*" or a list of numbers. If not "*", scan the list of temporary failure codes; if any match, the result is DEFER. */ if (ob->temp_errors[0] == '*') addr->transport_return = DEFER; else { const uschar *s = ob->temp_errors; uschar *p; uschar buffer[64]; int sep = 0; addr->transport_return = FAIL; while ((p = string_nextinlist(&s,&sep,buffer,sizeof(buffer)))) if (rc == Uatoi(p)) { addr->transport_return = DEFER; break; } } /* Ensure the message contains the expanded command and arguments. This doesn't have to be brilliantly efficient - it is an error situation. */ addr->message = string_sprintf("Child process of %s transport returned " "%d", tblock->name, rc); ptr = Ustrlen(addr->message); size = ptr + 1; /* If the return code is > 128, it often means that a shell command was terminated by a signal. */ ss = (rc > 128)? string_sprintf("(could mean shell command ended by signal %d (%s))", rc-128, os_strsignal(rc-128)) : US os_strexit(rc); if (*ss != 0) { addr->message = string_cat(addr->message, &size, &ptr, US" ", 1); addr->message = string_cat(addr->message, &size, &ptr, ss, Ustrlen(ss)); } /* Now add the command and arguments */ addr->message = string_cat(addr->message, &size, &ptr, US" from command:", 14); for (i = 0; i < sizeof(argv)/sizeof(int *) && argv[i] != NULL; i++) { BOOL quote = FALSE; addr->message = string_cat(addr->message, &size, &ptr, US" ", 1); if (Ustrpbrk(argv[i], " \t") != NULL) { quote = TRUE; addr->message = string_cat(addr->message, &size, &ptr, US"\"", 1); } addr->message = string_cat(addr->message, &size, &ptr, argv[i], Ustrlen(argv[i])); if (quote) addr->message = string_cat(addr->message, &size, &ptr, US"\"", 1); } /* Add previous filter timeout message, if present. */ if (*tmsg != 0) addr->message = string_cat(addr->message, &size, &ptr, tmsg, Ustrlen(tmsg)); addr->message[ptr] = 0; /* Ensure concatenated string terminated */ } } } /* Ensure all subprocesses (in particular, the output handling process) are complete before we pass this point. */ while (wait(&rc) >= 0); DEBUG(D_transport) debug_printf("%s transport yielded %d\n", tblock->name, addr->transport_return); /* If there has been a problem, the message in addr->message contains details of the pipe command. We don't want to expose these to the world, so we set up something bland to return to the sender. */ if (addr->transport_return != OK) addr->user_message = US"local delivery failed"; return FALSE; }
int auth_dovecot_server(auth_instance * ablock, uschar * data) { auth_dovecot_options_block *ob = (auth_dovecot_options_block *) ablock->options_block; struct sockaddr_un sa; uschar buffer[DOVECOT_AUTH_MAXLINELEN]; uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT]; uschar *auth_command; uschar *auth_extra_data = US""; uschar *p; int nargs, tmp; int crequid = 1, cont = 1, fd = -1, ret = DEFER; BOOL found = FALSE, have_mech_line = FALSE; HDEBUG(D_auth) debug_printf("dovecot authentication\n"); if (!data) { ret = FAIL; goto out; } memset(&sa, 0, sizeof(sa)); sa.sun_family = AF_UNIX; /* This was the original code here: it is nonsense because strncpy() does not return an integer. I have converted this to use the function that formats and checks length. PH */ /* if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) { } */ if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s", ob->server_socket)) { auth_defer_msg = US"authentication socket path too long"; return DEFER; } auth_defer_msg = US"authentication socket connection error"; if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) return DEFER; if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0) goto out; auth_defer_msg = US"authentication socket protocol error"; socket_buffer_left = 0; /* Global, used to read more than a line but return by line */ while (cont) { if (dc_gets(buffer, sizeof(buffer), fd) == NULL) OUT("authentication socket read error or premature eof"); p = buffer + Ustrlen(buffer) - 1; if (*p != '\n') OUT("authentication socket protocol line too long"); *p = '\0'; HDEBUG(D_auth) debug_printf("received: %s\n", buffer); nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0])); /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */ /* Code below rewritten by Kirill Miazine ([email protected]). Only check commands that Exim will need. Original code also failed if Dovecot server sent unknown command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */ /* pdp: note that CUID is a per-connection identifier sent by the server, which increments at server discretion. By contrast, the "id" field of the protocol is a connection-specific request identifier, which needs to be unique per request from the client and is not connected to the CUID value, so we ignore CUID from server. It's purely for diagnostics. */ if (Ustrcmp(args[0], US"VERSION") == 0) { CHECK_COMMAND("VERSION", 2, 2); if (Uatoi(args[1]) != VERSION_MAJOR) OUT("authentication socket protocol version mismatch"); } else if (Ustrcmp(args[0], US"MECH") == 0) { CHECK_COMMAND("MECH", 1, INT_MAX); have_mech_line = TRUE; if (strcmpic(US args[1], ablock->public_name) == 0) found = TRUE; } else if (Ustrcmp(args[0], US"SPID") == 0) { /* Unfortunately the auth protocol handshake wasn't designed well to differentiate between auth-client/userdb/master. auth-userdb and auth-master send VERSION + SPID lines only and nothing afterwards, while auth-client sends VERSION + MECH + SPID + CUID + more. The simplest way that we can determine if we've connected to the correct socket is to see if MECH line exists or not (alternatively we'd have to have a small timeout after SPID to see if CUID is sent or not). */ if (!have_mech_line) OUT("authentication socket type mismatch" " (connected to auth-master instead of auth-client)"); } else if (Ustrcmp(args[0], US"DONE") == 0) { CHECK_COMMAND("DONE", 0, 0); cont = 0; } } if (!found) { auth_defer_msg = string_sprintf( "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name); goto out; } /* Added by PH: data must not contain tab (as it is b64 it shouldn't, but check for safety). */ if (Ustrchr(data, '\t') != NULL) { ret = FAIL; goto out; } /* Added by PH: extra fields when TLS is in use or if the TCP/IP connection is local. */ if (tls_in.cipher != NULL) auth_extra_data = string_sprintf("secured\t%s%s", tls_in.certificate_verified? "valid-client-cert" : "", tls_in.certificate_verified? "\t" : ""); else if ( interface_address != NULL && Ustrcmp(sender_host_address, interface_address) == 0) auth_extra_data = US"secured\t"; /**************************************************************************** The code below was the original code here. It didn't work. A reading of the file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that this was not right. Maybe something changed. I changed it to move the service indication into the AUTH command, and it seems to be better. PH fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n" "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n", VERSION_MAJOR, VERSION_MINOR, getpid(), cuid, ablock->public_name, sender_host_address, interface_address, data ? CS data : ""); Subsequently, the command was modified to add "secured" and "valid-client- cert" when relevant. ****************************************************************************/ auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n" "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n", VERSION_MAJOR, VERSION_MINOR, getpid(), crequid, ablock->public_name, auth_extra_data, sender_host_address, interface_address, data); if (write(fd, auth_command, Ustrlen(auth_command)) < 0) HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n", strerror(errno)); HDEBUG(D_auth) debug_printf("sent: %s", auth_command); while (1) { uschar *temp; uschar *auth_id_pre = NULL; int i; if (dc_gets(buffer, sizeof(buffer), fd) == NULL) { auth_defer_msg = US"authentication socket read error or premature eof"; goto out; } buffer[Ustrlen(buffer) - 1] = 0; HDEBUG(D_auth) debug_printf("received: %s\n", buffer); nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0])); if (Uatoi(args[1]) != crequid) OUT("authentication socket connection id mismatch"); switch (toupper(*args[0])) { case 'C': CHECK_COMMAND("CONT", 1, 2); if ((tmp = auth_get_no64_data(&data, US args[2])) != OK) { ret = tmp; goto out; } /* Added by PH: data must not contain tab (as it is b64 it shouldn't, but check for safety). */ if (Ustrchr(data, '\t') != NULL) { ret = FAIL; goto out; } temp = string_sprintf("CONT\t%d\t%s\n", crequid, data); if (write(fd, temp, Ustrlen(temp)) < 0) OUT("authentication socket write error"); break; case 'F': CHECK_COMMAND("FAIL", 1, -1); for (i=2; (i<nargs) && (auth_id_pre == NULL); i++) { if ( Ustrncmp(args[i], US"user="******"OK", 2, -1); /* Search for the "user=$USER" string in the args array and return the proper value. */ for (i=2; (i<nargs) && (auth_id_pre == NULL); i++) { if ( Ustrncmp(args[i], US"user="******"authentication socket protocol error, username missing"); ret = OK; /* fallthrough */ default: goto out; } } out: /* close the socket used by dovecot */ if (fd >= 0) close(fd); /* Expand server_condition as an authorization check */ return ret == OK ? auth_check_serv_cond(ablock) : ret; }
int main(int argc, char **cargv) { open_db dbblock[8]; int max_db = sizeof(dbblock)/sizeof(open_db); int current = -1; int showtime = 0; int i; dbdata_wait *dbwait = NULL; uschar **argv = USS cargv; uschar buffer[256]; uschar structbuffer[1024]; if (argc != 2) { printf("Usage: test_dbfn directory\n"); printf("The subdirectory called \"db\" in the given directory is used for\n"); printf("the files used in this test program.\n"); return 1; } /* Initialize */ spool_directory = argv[1]; debug_selector = D_all - D_memory; debug_file = stderr; big_buffer = malloc(big_buffer_size); for (i = 0; i < max_db; i++) dbblock[i].dbptr = NULL; printf("\nExim's db functions tester: interface type is %s\n", EXIM_DBTYPE); printf("DBM library: "); #ifdef DB_VERSION_STRING printf("Berkeley DB: %s\n", DB_VERSION_STRING); #elif defined(BTREEVERSION) && defined(HASHVERSION) #ifdef USE_DB printf("probably Berkeley DB version 1.8x (native mode)\n"); #else printf("probably Berkeley DB version 1.8x (compatibility mode)\n"); #endif #elif defined(_DBM_RDONLY) || defined(dbm_dirfno) printf("probably ndbm\n"); #elif defined(USE_TDB) printf("using tdb\n"); #else #ifdef USE_GDBM printf("probably GDBM (native mode)\n"); #else printf("probably GDBM (compatibility mode)\n"); #endif #endif /* Test the functions */ printf("\nTest the functions\n> "); while (Ufgets(buffer, 256, stdin) != NULL) { int len = Ustrlen(buffer); int count = 1; clock_t start = 1; clock_t stop = 0; uschar *cmd = buffer; while (len > 0 && isspace((uschar)buffer[len-1])) len--; buffer[len] = 0; if (isdigit((uschar)*cmd)) { count = Uatoi(cmd); while (isdigit((uschar)*cmd)) cmd++; while (isspace((uschar)*cmd)) cmd++; } if (Ustrncmp(cmd, "open", 4) == 0) { int i; open_db *odb; uschar *s = cmd + 4; while (isspace((uschar)*s)) s++; for (i = 0; i < max_db; i++) if (dbblock[i].dbptr == NULL) break; if (i >= max_db) { printf("Too many open databases\n> "); continue; } start = clock(); odb = dbfn_open(s, O_RDWR, dbblock + i, TRUE); stop = clock(); if (odb != NULL) { current = i; printf("opened %d\n", current); } /* Other error cases will have written messages */ else if (errno == ENOENT) { printf("open failed: %s%s\n", strerror(errno), #ifdef USE_DB " (or other Berkeley DB error)" #else "" #endif ); } } else if (Ustrncmp(cmd, "write", 5) == 0) { int rc = 0; uschar *key = cmd + 5; uschar *data; if (current < 0) { printf("No current database\n"); continue; } while (isspace((uschar)*key)) key++; data = key; while (*data != 0 && !isspace((uschar)*data)) data++; *data++ = 0; while (isspace((uschar)*data)) data++; dbwait = (dbdata_wait *)(&structbuffer); Ustrcpy(dbwait->text, data); start = clock(); while (count-- > 0) rc = dbfn_write(dbblock + current, key, dbwait, Ustrlen(data) + sizeof(dbdata_wait)); stop = clock(); if (rc != 0) printf("Failed: %s\n", strerror(errno)); } else if (Ustrncmp(cmd, "read", 4) == 0) { uschar *key = cmd + 4; if (current < 0) { printf("No current database\n"); continue; } while (isspace((uschar)*key)) key++; start = clock(); while (count-- > 0) dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock+ current, key, NULL); stop = clock(); printf("%s\n", (dbwait == NULL)? "<not found>" : CS dbwait->text); } else if (Ustrncmp(cmd, "delete", 6) == 0) { uschar *key = cmd + 6; if (current < 0) { printf("No current database\n"); continue; } while (isspace((uschar)*key)) key++; dbfn_delete(dbblock + current, key); } else if (Ustrncmp(cmd, "scan", 4) == 0) { EXIM_CURSOR *cursor; BOOL startflag = TRUE; uschar *key; uschar keybuffer[256]; if (current < 0) { printf("No current database\n"); continue; } start = clock(); while ((key = dbfn_scan(dbblock + current, startflag, &cursor)) != NULL) { startflag = FALSE; Ustrcpy(keybuffer, key); dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock + current, keybuffer, NULL); printf("%s: %s\n", keybuffer, dbwait->text); } stop = clock(); printf("End of scan\n"); } else if (Ustrncmp(cmd, "close", 5) == 0) { uschar *s = cmd + 5; while (isspace((uschar)*s)) s++; i = Uatoi(s); if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n"); else { start = clock(); dbfn_close(dbblock + i); stop = clock(); dbblock[i].dbptr = NULL; if (i == current) current = -1; } } else if (Ustrncmp(cmd, "file", 4) == 0) { uschar *s = cmd + 4; while (isspace((uschar)*s)) s++; i = Uatoi(s); if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n"); else current = i; } else if (Ustrncmp(cmd, "time", 4) == 0) { showtime = ~showtime; printf("Timing %s\n", showtime? "on" : "off"); } else if (Ustrcmp(cmd, "q") == 0 || Ustrncmp(cmd, "quit", 4) == 0) break; else if (Ustrncmp(cmd, "help", 4) == 0) { printf("close [<number>] close file [<number>]\n"); printf("delete <key> remove record from current file\n"); printf("file <number> make file <number> current\n"); printf("open <name> open db file\n"); printf("q[uit] exit program\n"); printf("read <key> read record from current file\n"); printf("scan scan current file\n"); printf("time time display on/off\n"); printf("write <key> <rest-of-line> write record to current file\n"); } else printf("Eh?\n"); if (showtime && stop >= start) printf("start=%d stop=%d difference=%d\n", (int)start, (int)stop, (int)(stop - start)); printf("> "); } for (i = 0; i < max_db; i++) { if (dbblock[i].dbptr != NULL) { printf("\nClosing %d", i); dbfn_close(dbblock + i); } } printf("\n"); return 0; }
int main(int argc, char **cargv) { int dbdata_type; uschar **argv = USS cargv; uschar buffer[256]; uschar name[256]; void *reset_point = store_get(0); name[0] = 0; /* No name set */ /* Sort out the database type, verify what we are working on and then process user requests */ dbdata_type = check_args(argc, argv, US"fixdb", US""); printf("Modifying Exim hints database %s/db/%s\n", argv[1], argv[2]); for(;;) { open_db dbblock; open_db *dbm; void *record; dbdata_retry *retry; dbdata_wait *wait; dbdata_callout_cache *callout; dbdata_ratelimit *ratelimit; int i, oldlength; uschar *t; uschar field[256], value[256]; store_reset(reset_point); printf("> "); if (Ufgets(buffer, 256, stdin) == NULL) break; buffer[Ustrlen(buffer)-1] = 0; field[0] = value[0] = 0; /* If the buffer contains just one digit, or just consists of "d", use the previous name for an update. */ if ((isdigit((uschar)buffer[0]) && (buffer[1] == ' ' || buffer[1] == '\0')) || Ustrcmp(buffer, "d") == 0) { if (name[0] == 0) { printf("No previous record name is set\n"); continue; } (void)sscanf(CS buffer, "%s %s", field, value); } else { name[0] = 0; (void)sscanf(CS buffer, "%s %s %s", name, field, value); } /* Handle an update request */ if (field[0] != 0) { int verify = 1; dbm = dbfn_open(argv[1], argv[2], O_RDWR, &dbblock); if (dbm == NULL) continue; if (Ustrcmp(field, "d") == 0) { if (value[0] != 0) printf("unexpected value after \"d\"\n"); else printf("%s\n", (dbfn_delete(dbm, name) < 0)? "not found" : "deleted"); dbfn_close(dbm); continue; } else if (isdigit((uschar)field[0])) { int fieldno = Uatoi(field); if (value[0] == 0) { printf("value missing\n"); dbfn_close(dbm); continue; } else { record = dbfn_read_with_length(dbm, name, &oldlength); if (record == NULL) printf("not found\n"); else { time_t tt; int length = 0; /* Stops compiler warning */ switch(dbdata_type) { case type_retry: retry = (dbdata_retry *)record; length = sizeof(dbdata_retry) + Ustrlen(retry->text); switch(fieldno) { case 0: retry->basic_errno = Uatoi(value); break; case 1: retry->more_errno = Uatoi(value); break; case 2: if ((tt = read_time(value)) > 0) retry->first_failed = tt; else printf("bad time value\n"); break; case 3: if ((tt = read_time(value)) > 0) retry->last_try = tt; else printf("bad time value\n"); break; case 4: if ((tt = read_time(value)) > 0) retry->next_try = tt; else printf("bad time value\n"); break; case 5: if (Ustrcmp(value, "yes") == 0) retry->expired = TRUE; else if (Ustrcmp(value, "no") == 0) retry->expired = FALSE; else printf("\"yes\" or \"no\" expected=n"); break; default: printf("unknown field number\n"); verify = 0; break; } break; case type_wait: printf("Can't change contents of wait database record\n"); break; case type_misc: printf("Can't change contents of misc database record\n"); break; case type_callout: callout = (dbdata_callout_cache *)record; length = sizeof(dbdata_callout_cache); switch(fieldno) { case 0: callout->result = Uatoi(value); break; case 1: callout->postmaster_result = Uatoi(value); break; case 2: callout->random_result = Uatoi(value); break; default: printf("unknown field number\n"); verify = 0; break; } break; case type_ratelimit: ratelimit = (dbdata_ratelimit *)record; length = sizeof(dbdata_ratelimit); switch(fieldno) { case 0: if ((tt = read_time(value)) > 0) ratelimit->time_stamp = tt; else printf("bad time value\n"); break; case 1: ratelimit->time_usec = Uatoi(value); break; case 2: ratelimit->rate = Ustrtod(value, NULL); break; default: printf("unknown field number\n"); verify = 0; break; } break; } dbfn_write(dbm, name, record, length); } } } else { printf("field number or d expected\n"); verify = 0; } dbfn_close(dbm); if (!verify) continue; } /* The "name" q causes an exit */ else if (Ustrcmp(name, "q") == 0) return 0; /* Handle a read request, or verify after an update. */ dbm = dbfn_open(argv[1], argv[2], O_RDONLY, &dbblock); if (dbm == NULL) continue; record = dbfn_read_with_length(dbm, name, &oldlength); if (record == NULL) { printf("record %s not found\n", name); name[0] = 0; } else { int count_bad = 0; printf("%s\n", CS print_time(((dbdata_generic *)record)->time_stamp)); switch(dbdata_type) { case type_retry: retry = (dbdata_retry *)record; printf("0 error number: %d %s\n", retry->basic_errno, retry->text); printf("1 extra data: %d\n", retry->more_errno); printf("2 first failed: %s\n", print_time(retry->first_failed)); printf("3 last try: %s\n", print_time(retry->last_try)); printf("4 next try: %s\n", print_time(retry->next_try)); printf("5 expired: %s\n", (retry->expired)? "yes" : "no"); break; case type_wait: wait = (dbdata_wait *)record; t = wait->text; printf("Sequence: %d\n", wait->sequence); if (wait->count > WAIT_NAME_MAX) { printf("**** Data corrupted: count=%d=0x%x max=%d ****\n", wait->count, wait->count, WAIT_NAME_MAX); wait->count = WAIT_NAME_MAX; count_bad = 1; } for (i = 1; i <= wait->count; i++) { Ustrncpy(value, t, MESSAGE_ID_LENGTH); value[MESSAGE_ID_LENGTH] = 0; if (count_bad && value[0] == 0) break; if (Ustrlen(value) != MESSAGE_ID_LENGTH || Ustrspn(value, "0123456789" "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ-") != MESSAGE_ID_LENGTH) { int j; printf("\n**** Data corrupted: bad character in message id ****\n"); for (j = 0; j < MESSAGE_ID_LENGTH; j++) printf("%02x ", value[j]); printf("\n"); break; } printf("%s ", value); t += MESSAGE_ID_LENGTH; } printf("\n"); break; case type_misc: break; case type_callout: callout = (dbdata_callout_cache *)record; printf("0 callout: %s (%d)\n", print_cache(callout->result), callout->result); if (oldlength > sizeof(dbdata_callout_cache_address)) { printf("1 postmaster: %s (%d)\n", print_cache(callout->postmaster_result), callout->postmaster_result); printf("2 random: %s (%d)\n", print_cache(callout->random_result), callout->random_result); } break; case type_ratelimit: ratelimit = (dbdata_ratelimit *)record; printf("0 time stamp: %s\n", print_time(ratelimit->time_stamp)); printf("1 fract. time: .%06d\n", ratelimit->time_usec); printf("2 sender rate: % .3f\n", ratelimit->rate); break; } } /* The database is closed after each request */ dbfn_close(dbm); } printf("\n"); return 0; }
static int perform_mysql_search(const uschar *query, uschar *server, uschar **resultptr, uschar **errmsg, BOOL *defer_break, BOOL *do_cache) { MYSQL *mysql_handle = NULL; /* Keep compilers happy */ MYSQL_RES *mysql_result = NULL; MYSQL_ROW mysql_row_data; MYSQL_FIELD *fields; int i; int ssize = 0; int offset = 0; int yield = DEFER; unsigned int num_fields; uschar *result = NULL; mysql_connection *cn; uschar *server_copy = NULL; uschar *sdata[4]; /* Disaggregate the parameters from the server argument. The order is host, database, user, password. We can write to the string, since it is in a nextinlist temporary buffer. The copy of the string that is used for caching has the password removed. This copy is also used for debugging output. */ for (i = 3; i > 0; i--) { uschar *pp = Ustrrchr(server, '/'); if (pp == NULL) { *errmsg = string_sprintf("incomplete MySQL server data: %s", (i == 3)? server : server_copy); *defer_break = TRUE; return DEFER; } *pp++ = 0; sdata[i] = pp; if (i == 3) server_copy = string_copy(server); /* sans password */ } sdata[0] = server; /* What's left at the start */ /* See if we have a cached connection to the server */ for (cn = mysql_connections; cn != NULL; cn = cn->next) { if (Ustrcmp(cn->server, server_copy) == 0) { mysql_handle = cn->handle; break; } } /* If no cached connection, we must set one up. Mysql allows for a host name and port to be specified. It also allows the name of a Unix socket to be used. Unfortunately, this contains slashes, but its use is expected to be rare, so the rather cumbersome syntax shouldn't inconvenience too many people. We use this: host:port(socket) where all the parts are optional. */ if (cn == NULL) { uschar *p; uschar *socket = NULL; int port = 0; if ((p = Ustrchr(sdata[0], '(')) != NULL) { *p++ = 0; socket = p; while (*p != 0 && *p != ')') p++; *p = 0; } if ((p = Ustrchr(sdata[0], ':')) != NULL) { *p++ = 0; port = Uatoi(p); } if (Ustrchr(sdata[0], '/') != NULL) { *errmsg = string_sprintf("unexpected slash in MySQL server hostname: %s", sdata[0]); *defer_break = TRUE; return DEFER; } /* If the database is the empty string, set it NULL - the query must then define it. */ if (sdata[1][0] == 0) sdata[1] = NULL; DEBUG(D_lookup) debug_printf("MYSQL new connection: host=%s port=%d socket=%s " "database=%s user=%s\n", sdata[0], port, socket, sdata[1], sdata[2]); /* Get store for a new handle, initialize it, and connect to the server */ mysql_handle = store_get(sizeof(MYSQL)); mysql_init(mysql_handle); if (mysql_real_connect(mysql_handle, /* host user passwd database */ CS sdata[0], CS sdata[2], CS sdata[3], CS sdata[1], port, CS socket, CLIENT_MULTI_RESULTS) == NULL) { *errmsg = string_sprintf("MYSQL connection failed: %s", mysql_error(mysql_handle)); *defer_break = FALSE; goto MYSQL_EXIT; } /* Add the connection to the cache */ cn = store_get(sizeof(mysql_connection)); cn->server = server_copy; cn->handle = mysql_handle; cn->next = mysql_connections; mysql_connections = cn; } /* Else use a previously cached connection */ else { DEBUG(D_lookup) debug_printf("MYSQL using cached connection for %s\n", server_copy); } /* Run the query */ if (mysql_query(mysql_handle, CS query) != 0) { *errmsg = string_sprintf("MYSQL: query failed: %s\n", mysql_error(mysql_handle)); *defer_break = FALSE; goto MYSQL_EXIT; } /* Pick up the result. If the query was not of the type that returns data, namely INSERT, UPDATE, or DELETE, an error occurs here. However, this situation can be detected by calling mysql_field_count(). If its result is zero, no data was expected (this is all explained clearly in the MySQL manual). In this case, we return the number of rows affected by the command. In this event, we do NOT want to cache the result; also the whole cache for the handle must be cleaned up. Setting do_cache FALSE requests this. */ if ((mysql_result = mysql_use_result(mysql_handle)) == NULL) { if ( mysql_field_count(mysql_handle) == 0 ) { DEBUG(D_lookup) debug_printf("MYSQL: query was not one that returns data\n"); result = string_sprintf("%d", mysql_affected_rows(mysql_handle)); *do_cache = FALSE; goto MYSQL_EXIT; } *errmsg = string_sprintf("MYSQL: lookup result failed: %s\n", mysql_error(mysql_handle)); *defer_break = FALSE; goto MYSQL_EXIT; } /* Find the number of fields returned. If this is one, we don't add field names to the data. Otherwise we do. */ num_fields = mysql_num_fields(mysql_result); /* Get the fields and construct the result string. If there is more than one row, we insert '\n' between them. */ fields = mysql_fetch_fields(mysql_result); while ((mysql_row_data = mysql_fetch_row(mysql_result)) != NULL) { unsigned long *lengths = mysql_fetch_lengths(mysql_result); if (result != NULL) result = string_cat(result, &ssize, &offset, US"\n", 1); if (num_fields == 1) { if (mysql_row_data[0] != NULL) /* NULL value yields nothing */ result = string_cat(result, &ssize, &offset, US mysql_row_data[0], lengths[0]); } else for (i = 0; i < num_fields; i++) { result = lf_quote(US fields[i].name, US mysql_row_data[i], lengths[i], result, &ssize, &offset); } } /* more results? -1 = no, >0 = error, 0 = yes (keep looping) This is needed because of the CLIENT_MULTI_RESULTS on mysql_real_connect(), we don't expect any more results. */ while((i = mysql_next_result(mysql_handle)) >= 0) { if(i == 0) { /* Just ignore more results */ DEBUG(D_lookup) debug_printf("MYSQL: got unexpected more results\n"); continue; } *errmsg = string_sprintf("MYSQL: lookup result error when checking for more results: %s\n", mysql_error(mysql_handle)); goto MYSQL_EXIT; } /* If result is NULL then no data has been found and so we return FAIL. Otherwise, we must terminate the string which has been built; string_cat() always leaves enough room for a terminating zero. */ if (result == NULL) { yield = FAIL; *errmsg = US"MYSQL: no data found"; } else { result[offset] = 0; store_reset(result + offset + 1); } /* Get here by goto from various error checks and from the case where no data was read (e.g. an update query). */ MYSQL_EXIT: /* Free mysal store for any result that was got; don't close the connection, as it is cached. */ if (mysql_result != NULL) mysql_free_result(mysql_result); /* Non-NULL result indicates a sucessful result */ if (result != NULL) { *resultptr = result; return OK; } else { DEBUG(D_lookup) debug_printf("%s\n", *errmsg); return yield; /* FAIL or DEFER */ } }
static int control_ldap_search(uschar *ldap_url, int search_type, uschar **res, uschar **errmsg) { BOOL defer_break = FALSE; int timelimit = LDAP_NO_LIMIT; int sizelimit = LDAP_NO_LIMIT; int tcplimit = 0; int sep = 0; int dereference = LDAP_DEREF_NEVER; void* referrals = LDAP_OPT_ON; uschar *url = ldap_url; uschar *p; uschar *user = NULL; uschar *password = NULL; uschar *server, *list; uschar buffer[512]; while (isspace(*url)) url++; /* Until the string begins "ldap", search for the other parameter settings that are recognized. They are of the form NAME=VALUE, with the value being optionally double-quoted. There must still be a space after it, however. No NAME has the value "ldap". */ while (strncmpic(url, US"ldap", 4) != 0) { uschar *name = url; while (*url != 0 && *url != '=') url++; if (*url == '=') { int namelen; uschar *value; namelen = ++url - name; value = string_dequote(&url); if (isspace(*url)) { if (strncmpic(name, US"USER="******"PASS="******"SIZE=", namelen) == 0) sizelimit = Uatoi(value); else if (strncmpic(name, US"TIME=", namelen) == 0) timelimit = Uatoi(value); else if (strncmpic(name, US"CONNECT=", namelen) == 0) tcplimit = Uatoi(value); else if (strncmpic(name, US"NETTIME=", namelen) == 0) tcplimit = Uatoi(value); /* Don't know if all LDAP libraries have LDAP_OPT_DEREF */ #ifdef LDAP_OPT_DEREF else if (strncmpic(name, US"DEREFERENCE=", namelen) == 0) { if (strcmpic(value, US"never") == 0) dereference = LDAP_DEREF_NEVER; else if (strcmpic(value, US"searching") == 0) dereference = LDAP_DEREF_SEARCHING; else if (strcmpic(value, US"finding") == 0) dereference = LDAP_DEREF_FINDING; if (strcmpic(value, US"always") == 0) dereference = LDAP_DEREF_ALWAYS; } #else else if (strncmpic(name, US"DEREFERENCE=", namelen) == 0) { *errmsg = string_sprintf("LDAP_OP_DEREF not defined in this LDAP " "library - cannot use \"dereference\""); DEBUG(D_lookup) debug_printf("%s\n", *errmsg); return DEFER; } #endif #ifdef LDAP_OPT_REFERRALS else if (strncmpic(name, US"REFERRALS=", namelen) == 0) { if (strcmpic(value, US"follow") == 0) referrals = LDAP_OPT_ON; else if (strcmpic(value, US"nofollow") == 0) referrals = LDAP_OPT_OFF; else { *errmsg = string_sprintf("LDAP option REFERRALS is not \"follow\" " "or \"nofollow\""); DEBUG(D_lookup) debug_printf("%s\n", *errmsg); return DEFER; } } #else else if (strncmpic(name, US"REFERRALS=", namelen) == 0) { *errmsg = string_sprintf("LDAP_OP_REFERRALS not defined in this LDAP " "library - cannot use \"referrals\""); DEBUG(D_lookup) debug_printf("%s\n", *errmsg); return DEFER; } #endif else { *errmsg = string_sprintf("unknown parameter \"%.*s\" precedes LDAP URL", namelen, name); DEBUG(D_lookup) debug_printf("LDAP query error: %s\n", *errmsg); return DEFER; } while (isspace(*url)) url++; continue; } } *errmsg = US"malformed parameter setting precedes LDAP URL"; DEBUG(D_lookup) debug_printf("LDAP query error: %s\n", *errmsg); return DEFER; } /* If user is set, de-URL-quote it. Some LDAP libraries do this for themselves, but it seems that not all behave like this. The DN for the user is often the result of ${quote_ldap_dn:...} quoting, which does apply URL quoting, because that is needed when the DN is used as a base DN in a query. Sigh. This is all far too complicated. */ if (user != NULL) { uschar *s; uschar *t = user; for (s = user; *s != 0; s++) { int c, d; if (*s == '%' && isxdigit(c=s[1]) && isxdigit(d=s[2])) { c = tolower(c); d = tolower(d); *t++ = (((c >= 'a')? (10 + c - 'a') : c - '0') << 4) | ((d >= 'a')? (10 + d - 'a') : d - '0'); s += 2; } else *t++ = *s; } *t = 0; } DEBUG(D_lookup) debug_printf("LDAP parameters: user=%s pass=%s size=%d time=%d connect=%d " "dereference=%d referrals=%s\n", user, password, sizelimit, timelimit, tcplimit, dereference, (referrals == LDAP_OPT_ON)? "on" : "off"); /* If the request is just to check authentication, some credentials must be given. The password must not be empty because LDAP binds with an empty password are considered anonymous, and will succeed on most installations. */ if (search_type == SEARCH_LDAP_AUTH) { if (user == NULL || password == NULL) { *errmsg = US"ldapauth lookups must specify the username and password"; return DEFER; } if (password[0] == 0) { DEBUG(D_lookup) debug_printf("Empty password: ldapauth returns FAIL\n"); return FAIL; } } /* Check for valid ldap url starters */ p = url + 4; if (tolower(*p) == 's' || tolower(*p) == 'i') p++; if (Ustrncmp(p, "://", 3) != 0) { *errmsg = string_sprintf("LDAP URL does not start with \"ldap://\", " "\"ldaps://\", or \"ldapi://\" (it starts with \"%.16s...\")", url); DEBUG(D_lookup) debug_printf("LDAP query error: %s\n", *errmsg); return DEFER; } /* No default servers, or URL contains a server name: just one attempt */ if (eldap_default_servers == NULL || p[3] != '/') { return perform_ldap_search(url, NULL, 0, search_type, res, errmsg, &defer_break, user, password, sizelimit, timelimit, tcplimit, dereference, referrals); } /* Loop through the default servers until OK or FAIL */ list = eldap_default_servers; while ((server = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL) { int rc; int port = 0; uschar *colon = Ustrchr(server, ':'); if (colon != NULL) { *colon = 0; port = Uatoi(colon+1); } rc = perform_ldap_search(url, server, port, search_type, res, errmsg, &defer_break, user, password, sizelimit, timelimit, tcplimit, dereference, referrals); if (rc != DEFER || defer_break) return rc; } return DEFER; }
int auth_dovecot_server(auth_instance *ablock, uschar *data) { auth_dovecot_options_block *ob = (auth_dovecot_options_block *)(ablock->options_block); struct sockaddr_un sa; uschar buffer[4096]; uschar *args[8]; uschar *auth_command; uschar *auth_extra_data = US""; int nargs, tmp; int cuid = 0, cont = 1, found = 0, fd, ret = DEFER; HDEBUG(D_auth) debug_printf("dovecot authentication\n"); memset(&sa, 0, sizeof(sa)); sa.sun_family = AF_UNIX; /* This was the original code here: it is nonsense because strncpy() does not return an integer. I have converted this to use the function that formats and checks length. PH */ /* if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) { */ if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s", ob->server_socket)) { auth_defer_msg = US"authentication socket path too long"; return DEFER; } auth_defer_msg = US"authentication socket connection error"; fd = socket(PF_UNIX, SOCK_STREAM, 0); if (fd < 0) return DEFER; if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0) goto out; auth_defer_msg = US"authentication socket protocol error"; sbp = 0; /* Socket buffer pointer */ while (cont) { if (dc_gets(buffer, sizeof(buffer), fd) == NULL) OUT("authentication socket read error or premature eof"); buffer[Ustrlen(buffer) - 1] = 0; HDEBUG(D_auth) debug_printf("received: %s\n", buffer); nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0])); /* Code below rewritten by Kirill Miazine ([email protected]). Only check commands that Exim will need. Original code also failed if Dovecot server sent unknown command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */ if (Ustrcmp(args[0], US"CUID") == 0) { CHECK_COMMAND("CUID", 1, 1); cuid = Uatoi(args[1]); } else if (Ustrcmp(args[0], US"VERSION") == 0) { CHECK_COMMAND("VERSION", 2, 2); if (Uatoi(args[1]) != VERSION_MAJOR) OUT("authentication socket protocol version mismatch"); } else if (Ustrcmp(args[0], US"MECH") == 0) { CHECK_COMMAND("MECH", 1, INT_MAX); if (strcmpic(US args[1], ablock->public_name) == 0) found = 1; } else if (Ustrcmp(args[0], US"DONE") == 0) { CHECK_COMMAND("DONE", 0, 0); cont = 0; } } if (!found) goto out; /* Added by PH: data must not contain tab (as it is b64 it shouldn't, but check for safety). */ if (Ustrchr(data, '\t') != NULL) { ret = FAIL; goto out; } /* Added by PH: extra fields when TLS is in use or if the TCP/IP connection is local. */ if (tls_cipher != NULL) auth_extra_data = string_sprintf("secured\t%s%s", tls_certificate_verified? "valid-client-cert" : "", tls_certificate_verified? "\t" : ""); else if (interface_address != NULL && Ustrcmp(sender_host_address, interface_address) == 0) auth_extra_data = US"secured\t"; /**************************************************************************** The code below was the original code here. It didn't work. A reading of the file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that this was not right. Maybe something changed. I changed it to move the service indication into the AUTH command, and it seems to be better. PH fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n" "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n", VERSION_MAJOR, VERSION_MINOR, getpid(), cuid, ablock->public_name, sender_host_address, interface_address, data ? (char *) data : ""); Subsequently, the command was modified to add "secured" and "valid-client- cert" when relevant. The auth protocol is documented here: http://wiki.dovecot.org/Authentication_Protocol ****************************************************************************/ auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n" "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n", VERSION_MAJOR, VERSION_MINOR, getpid(), cuid, ablock->public_name, auth_extra_data, sender_host_address, interface_address, data ? (char *) data : ""); if (write(fd, auth_command, Ustrlen(auth_command)) < 0) HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n", strerror(errno)); HDEBUG(D_auth) debug_printf("sent: %s", auth_command); while (1) { uschar *temp; uschar *auth_id_pre = NULL; int i; if (dc_gets(buffer, sizeof(buffer), fd) == NULL) { auth_defer_msg = US"authentication socket read error or premature eof"; goto out; } buffer[Ustrlen(buffer) - 1] = 0; HDEBUG(D_auth) debug_printf("received: %s\n", buffer); nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0])); if (Uatoi(args[1]) != cuid) OUT("authentication socket connection id mismatch"); switch (toupper(*args[0])) { case 'C': CHECK_COMMAND("CONT", 1, 2); tmp = auth_get_no64_data(&data, US args[2]); if (tmp != OK) { ret = tmp; goto out; } /* Added by PH: data must not contain tab (as it is b64 it shouldn't, but check for safety). */ if (Ustrchr(data, '\t') != NULL) { ret = FAIL; goto out; } temp = string_sprintf("CONT\t%d\t%s\n", cuid, data); if (write(fd, temp, Ustrlen(temp)) < 0) OUT("authentication socket write error"); break; case 'F': CHECK_COMMAND("FAIL", 1, -1); for (i=2; (i<nargs) && (auth_id_pre == NULL); i++) { if ( Ustrncmp(args[i], US"user="******"OK", 2, -1); /* * Search for the "user=$USER" string in the args array * and return the proper value. */ for (i=2; (i<nargs) && (auth_id_pre == NULL); i++) { if ( Ustrncmp(args[i], US"user="******"authentication socket protocol error, username missing"); ret = OK; /* fallthrough */ default: goto out; } } out: /* close the socket used by dovecot */ if (fd >= 0) close(fd); /* Expand server_condition as an authorization check */ return (ret == OK)? auth_check_serv_cond(ablock) : ret; }