diff options
Diffstat (limited to 'pigeonhole/src/plugins/sieve-extprograms/sieve-extprograms-common.c')
-rw-r--r-- | pigeonhole/src/plugins/sieve-extprograms/sieve-extprograms-common.c | 648 |
1 files changed, 648 insertions, 0 deletions
diff --git a/pigeonhole/src/plugins/sieve-extprograms/sieve-extprograms-common.c b/pigeonhole/src/plugins/sieve-extprograms/sieve-extprograms-common.c new file mode 100644 index 0000000..3c9ff23 --- /dev/null +++ b/pigeonhole/src/plugins/sieve-extprograms/sieve-extprograms-common.c @@ -0,0 +1,648 @@ +/* Copyright (c) 2002-2018 Pigeonhole authors, see the included COPYING file + */ + +#include "lib.h" +#include "lib-signals.h" +#include "str.h" +#include "strfuncs.h" +#include "str-sanitize.h" +#include "unichar.h" +#include "array.h" +#include "eacces-error.h" +#include "smtp-params.h" +#include "istream.h" +#include "istream-crlf.h" +#include "istream-header-filter.h" +#include "ostream.h" +#include "mail-user.h" +#include "mail-storage.h" + +#include "program-client.h" + +#include "sieve-common.h" +#include "sieve-settings.h" +#include "sieve-error.h" +#include "sieve-extensions.h" +#include "sieve-ast.h" +#include "sieve-commands.h" +#include "sieve-stringlist.h" +#include "sieve-code.h" +#include "sieve-actions.h" +#include "sieve-validator.h" +#include "sieve-runtime.h" +#include "sieve-interpreter.h" + +#include "sieve-ext-copy.h" +#include "sieve-ext-variables.h" + +#include "sieve-extprograms-common.h" + +#include <unistd.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/socket.h> + +/* + * Limits + */ + +#define SIEVE_EXTPROGRAMS_MAX_PROGRAM_NAME_LEN 128 +#define SIEVE_EXTPROGRAMS_MAX_PROGRAM_ARG_LEN 1024 + +#define SIEVE_EXTPROGRAMS_DEFAULT_EXEC_TIMEOUT_SECS 10 +#define SIEVE_EXTPROGRAMS_CONNECT_TIMEOUT_MSECS 5 + +/* + * Pipe Extension Context + */ + +struct sieve_extprograms_config *sieve_extprograms_config_init +(const struct sieve_extension *ext) +{ + struct sieve_instance *svinst = ext->svinst; + struct sieve_extprograms_config *ext_config; + const char *extname = sieve_extension_name(ext); + const char *bin_dir, *socket_dir, *input_eol; + sieve_number_t execute_timeout; + + extname = strrchr(extname, '.'); + i_assert(extname != NULL); + extname++; + + bin_dir = sieve_setting_get + (svinst, t_strdup_printf("sieve_%s_bin_dir", extname)); + socket_dir = sieve_setting_get + (svinst, t_strdup_printf("sieve_%s_socket_dir", extname)); + input_eol = sieve_setting_get + (svinst, t_strdup_printf("sieve_%s_input_eol", extname)); + + ext_config = i_new(struct sieve_extprograms_config, 1); + ext_config->execute_timeout = + SIEVE_EXTPROGRAMS_DEFAULT_EXEC_TIMEOUT_SECS; + + if ( bin_dir == NULL && socket_dir == NULL ) { + e_debug(svinst->event, "%s extension: " + "no bin or socket directory specified; extension is unconfigured " + "(both sieve_%s_bin_dir and sieve_%s_socket_dir are not set)", + sieve_extension_name(ext), extname, extname); + } else { + ext_config->bin_dir = i_strdup(bin_dir); + ext_config->socket_dir = i_strdup(socket_dir); + + if (sieve_setting_get_duration_value + (svinst, t_strdup_printf("sieve_%s_exec_timeout", extname), + &execute_timeout)) { + ext_config->execute_timeout = execute_timeout; + } + + ext_config->default_input_eol = SIEVE_EXTPROGRAMS_EOL_CRLF; + if (input_eol != NULL && strcasecmp(input_eol, "lf") == 0) + ext_config->default_input_eol = SIEVE_EXTPROGRAMS_EOL_LF; + } + + if ( sieve_extension_is(ext, sieve_ext_vnd_pipe) ) + ext_config->copy_ext = sieve_ext_copy_get_extension(ext->svinst); + if ( sieve_extension_is(ext, sieve_ext_vnd_execute) ) + ext_config->var_ext = sieve_ext_variables_get_extension(ext->svinst); + return ext_config; +} + +void sieve_extprograms_config_deinit +(struct sieve_extprograms_config **ext_config) +{ + if ( *ext_config == NULL ) + return; + + i_free((*ext_config)->bin_dir); + i_free((*ext_config)->socket_dir); + i_free((*ext_config)); + + *ext_config = NULL; +} + +/* + * Program name and arguments + */ + +bool sieve_extprogram_name_is_valid(string_t *name) +{ + ARRAY_TYPE(unichars) uni_name; + unsigned int count, i; + const unichar_t *name_chars; + size_t namelen = str_len(name); + + /* Check minimum length */ + if ( namelen == 0 ) + return FALSE; + + /* Check worst-case maximum length */ + if ( namelen > SIEVE_EXTPROGRAMS_MAX_PROGRAM_NAME_LEN * 4 ) + return FALSE; + + /* Intialize array for unicode characters */ + t_array_init(&uni_name, namelen * 4); + + /* Convert UTF-8 to UCS4/UTF-32 */ + if ( uni_utf8_to_ucs4_n(str_data(name), namelen, &uni_name) < 0 ) + return FALSE; + name_chars = array_get(&uni_name, &count); + + /* Check true maximum length */ + if ( count > SIEVE_EXTPROGRAMS_MAX_PROGRAM_NAME_LEN ) + return FALSE; + + /* Scan name for invalid characters + * FIXME: compliance with Net-Unicode Definition (Section 2 of + * RFC 5198) is not checked fully and no normalization + * is performed. + */ + for ( i = 0; i < count; i++ ) { + + /* 0000-001F; [CONTROL CHARACTERS] */ + if ( name_chars[i] <= 0x001f ) + return FALSE; + + /* 002F; SLASH */ + if ( name_chars[i] == 0x002f ) + return FALSE; + + /* 007F; DELETE */ + if ( name_chars[i] == 0x007f ) + return FALSE; + + /* 0080-009F; [CONTROL CHARACTERS] */ + if ( name_chars[i] >= 0x0080 && name_chars[i] <= 0x009f ) + return FALSE; + + /* 00FF */ + if ( name_chars[i] == 0x00ff ) + return FALSE; + + /* 2028; LINE SEPARATOR */ + /* 2029; PARAGRAPH SEPARATOR */ + if ( name_chars[i] == 0x2028 || name_chars[i] == 0x2029 ) + return FALSE; + } + + return TRUE; +} + +bool sieve_extprogram_arg_is_valid(string_t *arg) +{ + const unsigned char *chars; + unsigned int i; + + /* Check maximum length */ + if ( str_len(arg) > SIEVE_EXTPROGRAMS_MAX_PROGRAM_ARG_LEN ) + return FALSE; + + /* Check invalid characters */ + chars = str_data(arg); + for ( i = 0; i < str_len(arg); i++ ) { + /* 0010; CR */ + if ( chars[i] == 0x0D ) + return FALSE; + + /* 0010; LF */ + if ( chars[i] == 0x0A ) + return FALSE; + } + + return TRUE; +} + +/* + * Command validation + */ + +struct _arg_validate_context { + struct sieve_validator *valdtr; + struct sieve_command *cmd; +}; + +static int _arg_validate +(void *context, struct sieve_ast_argument *item) +{ + struct _arg_validate_context *actx = (struct _arg_validate_context *) context; + + if ( sieve_argument_is_string_literal(item) ) { + string_t *arg = sieve_ast_argument_str(item); + + if ( !sieve_extprogram_arg_is_valid(arg) ) { + sieve_argument_validate_error(actx->valdtr, item, + "%s %s: specified external program argument `%s' is invalid", + sieve_command_identifier(actx->cmd), sieve_command_type_name(actx->cmd), + str_sanitize(str_c(arg), 128)); + + return -1; + } + } + + return 1; +} + +bool sieve_extprogram_command_validate +(struct sieve_validator *valdtr, struct sieve_command *cmd) +{ + struct sieve_ast_argument *arg = cmd->first_positional; + struct sieve_ast_argument *stritem; + struct _arg_validate_context actx; + string_t *program_name; + + if ( arg == NULL ) { + sieve_command_validate_error(valdtr, cmd, + "the %s %s expects at least one positional argument, but none was found", + sieve_command_identifier(cmd), sieve_command_type_name(cmd)); + return FALSE; + } + + /* <program-name: string> argument */ + + if ( !sieve_validate_positional_argument + (valdtr, cmd, arg, "program-name", 1, SAAT_STRING) ) { + return FALSE; + } + + if ( !sieve_validator_argument_activate(valdtr, cmd, arg, FALSE) ) + return FALSE; + + /* Variables are not allowed */ + if ( !sieve_argument_is_string_literal(arg) ) { + sieve_argument_validate_error(valdtr, arg, + "the %s %s requires a constant string " + "for its program-name argument", + sieve_command_identifier(cmd), sieve_command_type_name(cmd)); + return FALSE; + } + + /* Check program name */ + program_name = sieve_ast_argument_str(arg); + if ( !sieve_extprogram_name_is_valid(program_name) ) { + sieve_argument_validate_error(valdtr, arg, + "%s %s: invalid program name '%s'", + sieve_command_identifier(cmd), sieve_command_type_name(cmd), + str_sanitize(str_c(program_name), 80)); + return FALSE; + } + + /* Optional <arguments: string-list> argument */ + + arg = sieve_ast_argument_next(arg); + if ( arg == NULL ) + return TRUE; + + if ( !sieve_validate_positional_argument + (valdtr, cmd, arg, "arguments", 2, SAAT_STRING_LIST) ) { + return FALSE; + } + + if ( !sieve_validator_argument_activate(valdtr, cmd, arg, FALSE) ) + return FALSE; + + /* Check arguments */ + actx.valdtr = valdtr; + actx.cmd = cmd; + stritem = arg; + if ( sieve_ast_stringlist_map + (&stritem, (void *)&actx, _arg_validate) <= 0 ) { + return FALSE; + } + + if ( sieve_ast_argument_next(arg) != NULL ) { + sieve_command_validate_error(valdtr, cmd, + "the %s %s expects at most two positional arguments, but more were found", + sieve_command_identifier(cmd), sieve_command_type_name(cmd)); + return FALSE; + } + + return TRUE; +} + +/* + * Common command operands + */ + +int sieve_extprogram_command_read_operands +(const struct sieve_runtime_env *renv, sieve_size_t *address, + string_t **pname_r, struct sieve_stringlist **args_list_r) +{ + string_t *arg; + int ret; + + /* + * Read fixed operands + */ + + if ( (ret=sieve_opr_string_read + (renv, address, "program-name", pname_r)) <= 0 ) + return ret; + + if ( (ret=sieve_opr_stringlist_read_ex + (renv, address, "arguments", TRUE, args_list_r)) <= 0 ) + return ret; + + /* + * Check operands + */ + + arg = NULL; + while ( *args_list_r != NULL && + (ret=sieve_stringlist_next_item(*args_list_r, &arg)) > 0 ) { + if ( !sieve_extprogram_arg_is_valid(arg) ) { + sieve_runtime_error(renv, NULL, + "specified :args item `%s' is invalid", + str_sanitize(str_c(arg), 128)); + return SIEVE_EXEC_FAILURE; + } + } + + if ( ret < 0 ) { + sieve_runtime_trace_error(renv, "invalid args-list item"); + return SIEVE_EXEC_BIN_CORRUPT; + } + + return SIEVE_EXEC_OK; +} + +/* + * Running external programs + */ + +struct sieve_extprogram { + struct sieve_instance *svinst; + const struct sieve_extprograms_config *ext_config; + + const struct sieve_script_env *scriptenv; + struct program_client_settings set; + struct program_client *program_client; +}; + +void sieve_extprogram_exec_error +(struct sieve_error_handler *ehandler, const char *location, + const char *fmt, ...) +{ + char str[256]; + struct tm *tm; + const char *timestamp; + + tm = localtime(&ioloop_time); + + timestamp = + ( strftime(str, sizeof(str), " [%Y-%m-%d %H:%M:%S]", tm) > 0 ? str : "" ); + + va_list args; + va_start(args, fmt); + + T_BEGIN { + sieve_error(ehandler, location, + "%s: refer to server log for more information.%s", + t_strdup_vprintf(fmt, args), timestamp); + } T_END; + + va_end(args); +} + +/* API */ + +struct sieve_extprogram *sieve_extprogram_create +(const struct sieve_extension *ext, const struct sieve_script_env *senv, + const struct sieve_message_data *msgdata, const char *action, + const char *program_name, const char * const *args, + enum sieve_error *error_r) +{ + struct sieve_instance *svinst = ext->svinst; + struct sieve_extprograms_config *ext_config = + (struct sieve_extprograms_config *) ext->context; + const struct smtp_address *sender, *recipient, *orig_recipient; + struct sieve_extprogram *sprog; + const char *path = NULL; + struct stat st; + bool fork = FALSE; + + e_debug(svinst->event, "action %s: " + "running program: %s", action, program_name); + + if ( ext_config == NULL || + (ext_config->bin_dir == NULL && ext_config->socket_dir == NULL) ) { + e_error(svinst->event, "action %s: " + "failed to execute program `%s': " + "vnd.dovecot.%s extension is unconfigured", + action, program_name, action); + *error_r = SIEVE_ERROR_NOT_FOUND; + return NULL; + } + + /* Try socket first */ + if ( ext_config->socket_dir != NULL ) { + path = t_strconcat(senv->user->set->base_dir, "/", + ext_config->socket_dir, "/", program_name, NULL); + if ( stat(path, &st) < 0 ) { + switch ( errno ) { + case ENOENT: + e_debug(svinst->event, "action %s: " + "socket path `%s' for program `%s' not found", + action, path, program_name); + break; + case EACCES: + e_error(svinst->event, "action %s: " + "failed to stat socket: %s", + action, eacces_error_get("stat", path)); + *error_r = SIEVE_ERROR_NO_PERMISSION; + return NULL; + default: + e_error(svinst->event, "action %s: " + "failed to stat socket `%s': %m", + action, path); + *error_r = SIEVE_ERROR_TEMP_FAILURE; + return NULL; + } + path = NULL; + } else if ( !S_ISSOCK(st.st_mode) ) { + e_error(svinst->event, "action %s: " + "socket path `%s' for program `%s' is not a socket", + action, path, program_name); + *error_r = SIEVE_ERROR_NOT_POSSIBLE; + return NULL; + } + } + + /* Try executable next */ + if ( path == NULL && ext_config->bin_dir != NULL ) { + fork = TRUE; + path = t_strconcat(ext_config->bin_dir, "/", program_name, NULL); + if ( stat(path, &st) < 0 ) { + switch ( errno ) { + case ENOENT: + e_debug(svinst->event, "action %s: " + "executable path `%s' for program `%s' not found", + action, path, program_name); + *error_r = SIEVE_ERROR_NOT_FOUND; + break; + case EACCES: + e_error(svinst->event, "action %s: " + "failed to stat program: %s", + action, eacces_error_get("stat", path)); + *error_r = SIEVE_ERROR_NO_PERMISSION; + break; + default: + e_error(svinst->event, "action %s: " + "failed to stat program `%s': %m", + action, path); + *error_r = SIEVE_ERROR_TEMP_FAILURE; + break; + } + + return NULL; + } else if ( !S_ISREG(st.st_mode) ) { + e_error(svinst->event, "action %s: " + "executable `%s' for program `%s' is not a regular file", + action, path, program_name); + *error_r = SIEVE_ERROR_NOT_POSSIBLE; + return NULL; + } else if ( (st.st_mode & S_IWOTH) != 0 ) { + e_error(svinst->event, "action %s: " + "executable `%s' for program `%s' is world-writable", + action, path, program_name); + *error_r = SIEVE_ERROR_NO_PERMISSION; + return NULL; + } + } + + /* None found ? */ + if ( path == NULL ) { + e_error(svinst->event, "action %s: " + "program `%s' not found", action, program_name); + *error_r = SIEVE_ERROR_NOT_FOUND; + return NULL; + } + + sprog = i_new(struct sieve_extprogram, 1); + sprog->svinst = ext->svinst; + sprog->ext_config = ext_config; + sprog->scriptenv = senv; + + sprog->set.client_connect_timeout_msecs = + SIEVE_EXTPROGRAMS_CONNECT_TIMEOUT_MSECS; + sprog->set.input_idle_timeout_msecs = + ext_config->execute_timeout * 1000; + restrict_access_init(&sprog->set.restrict_set); + if (senv->user->uid != 0) + sprog->set.restrict_set.uid = senv->user->uid; + if (senv->user->gid != 0) + sprog->set.restrict_set.gid = senv->user->gid; + sprog->set.debug = svinst->debug; + + if ( fork ) { + sprog->program_client = + program_client_local_create(path, args, &sprog->set); + } else { + sprog->program_client = + program_client_unix_create(path, args, &sprog->set, FALSE); + } + + if ( svinst->username != NULL ) + program_client_set_env(sprog->program_client, "USER", svinst->username); + if ( svinst->home_dir != NULL ) + program_client_set_env(sprog->program_client, "HOME", svinst->home_dir); + if ( svinst->hostname != NULL ) + program_client_set_env(sprog->program_client, "HOST", svinst->hostname); + + sender = msgdata->envelope.mail_from; + recipient = msgdata->envelope.rcpt_to; + orig_recipient = NULL; + if ( msgdata->envelope.rcpt_params != NULL ) + orig_recipient = msgdata->envelope.rcpt_params->orcpt.addr; + + if ( !smtp_address_isnull(sender) ) { + program_client_set_env(sprog->program_client, "SENDER", + smtp_address_encode(sender)); + } + if ( !smtp_address_isnull(recipient) ) { + program_client_set_env(sprog->program_client, "RECIPIENT", + smtp_address_encode(recipient)); + } + if ( !smtp_address_isnull(orig_recipient) ) { + program_client_set_env(sprog->program_client, "ORIG_RECIPIENT", + smtp_address_encode(orig_recipient)); + } + + return sprog; +} + +void sieve_extprogram_destroy(struct sieve_extprogram **_sprog) +{ + struct sieve_extprogram *sprog = *_sprog; + + program_client_destroy(&sprog->program_client); + i_free(sprog); + *_sprog = NULL; +} + +/* I/0 */ + +void sieve_extprogram_set_output +(struct sieve_extprogram *sprog, struct ostream *output) +{ + program_client_set_output(sprog->program_client, output); +} + +void sieve_extprogram_set_input +(struct sieve_extprogram *sprog, struct istream *input) +{ + switch (sprog->ext_config->default_input_eol) { + case SIEVE_EXTPROGRAMS_EOL_LF: + input = i_stream_create_lf(input); + break; + case SIEVE_EXTPROGRAMS_EOL_CRLF: + input = i_stream_create_crlf(input); + break; + default: + i_unreached(); + } + + program_client_set_input(sprog->program_client, input); + + i_stream_unref(&input); +} + +void sieve_extprogram_set_output_seekable +(struct sieve_extprogram *sprog) +{ + string_t *prefix; + prefix = t_str_new(128); + mail_user_set_get_temp_prefix(prefix, sprog->scriptenv->user->set); + + program_client_set_output_seekable(sprog->program_client, str_c(prefix)); +} + +struct istream *sieve_extprogram_get_output_seekable +(struct sieve_extprogram *sprog) +{ + return program_client_get_output_seekable(sprog->program_client); +} + +int sieve_extprogram_set_input_mail +(struct sieve_extprogram *sprog, struct mail *mail) +{ + struct istream *input; + + if (mail_get_stream(mail, NULL, NULL, &input) < 0) + return -1; + + sieve_extprogram_set_input(sprog, input); + return 1; +} + +int sieve_extprogram_run(struct sieve_extprogram *sprog) +{ + switch (program_client_run(sprog->program_client)) { + case PROGRAM_CLIENT_EXIT_STATUS_INTERNAL_FAILURE: + return -1; + case PROGRAM_CLIENT_EXIT_STATUS_FAILURE: + return 0; + case PROGRAM_CLIENT_EXIT_STATUS_SUCCESS: + return 1; + } + i_unreached(); +} + |