diff options
Diffstat (limited to 'src/auth/auth-request-handler.c')
-rw-r--r-- | src/auth/auth-request-handler.c | 985 |
1 files changed, 985 insertions, 0 deletions
diff --git a/src/auth/auth-request-handler.c b/src/auth/auth-request-handler.c new file mode 100644 index 0000000..d4bf53c --- /dev/null +++ b/src/auth/auth-request-handler.c @@ -0,0 +1,985 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "ioloop.h" +#include "array.h" +#include "aqueue.h" +#include "base64.h" +#include "hash.h" +#include "net.h" +#include "str.h" +#include "strescape.h" +#include "str-sanitize.h" +#include "master-interface.h" +#include "auth-penalty.h" +#include "auth-request.h" +#include "auth-token.h" +#include "auth-client-connection.h" +#include "auth-master-connection.h" +#include "auth-request-handler.h" +#include "auth-request-handler-private.h" +#include "auth-policy.h" + +#define AUTH_FAILURE_DELAY_CHECK_MSECS 500 +static ARRAY(struct auth_request *) auth_failures_arr; +static struct aqueue *auth_failures; +static struct timeout *to_auth_failures; + +static void auth_failure_timeout(void *context) ATTR_NULL(1); + + +static void +auth_request_handler_default_reply_callback(struct auth_request *request, + enum auth_client_result result, + const void *auth_reply, + size_t reply_size); + +static void +auth_request_handler_default_reply_continue(struct auth_request *request, + const void *reply, + size_t reply_size); + +struct auth_request_handler * +auth_request_handler_create(bool token_auth, auth_client_request_callback_t *callback, + struct auth_client_connection *conn, + auth_master_request_callback_t *master_callback) +{ + struct auth_request_handler *handler; + pool_t pool; + + pool = pool_alloconly_create("auth request handler", 4096); + + handler = p_new(pool, struct auth_request_handler, 1); + handler->refcount = 1; + handler->pool = pool; + hash_table_create_direct(&handler->requests, pool, 0); + handler->callback = callback; + handler->conn = conn; + handler->master_callback = master_callback; + handler->token_auth = token_auth; + handler->reply_callback = + auth_request_handler_default_reply_callback; + handler->reply_continue_callback = + auth_request_handler_default_reply_continue; + handler->verify_plain_continue_callback = + auth_request_default_verify_plain_continue; + return handler; +} + +unsigned int +auth_request_handler_get_request_count(struct auth_request_handler *handler) +{ + return hash_table_count(handler->requests); +} + +void auth_request_handler_abort_requests(struct auth_request_handler *handler) +{ + struct hash_iterate_context *iter; + void *key; + struct auth_request *auth_request; + + iter = hash_table_iterate_init(handler->requests); + while (hash_table_iterate(iter, handler->requests, &key, &auth_request)) { + switch (auth_request->state) { + case AUTH_REQUEST_STATE_NEW: + case AUTH_REQUEST_STATE_MECH_CONTINUE: + case AUTH_REQUEST_STATE_FINISHED: + auth_request->removed_from_handler = TRUE; + auth_request_unref(&auth_request); + hash_table_remove(handler->requests, key); + break; + case AUTH_REQUEST_STATE_PASSDB: + case AUTH_REQUEST_STATE_USERDB: + /* can't abort a pending passdb/userdb lookup */ + break; + case AUTH_REQUEST_STATE_MAX: + i_unreached(); + } + } + hash_table_iterate_deinit(&iter); +} + +void auth_request_handler_unref(struct auth_request_handler **_handler) +{ + struct auth_request_handler *handler = *_handler; + + *_handler = NULL; + + i_assert(handler->refcount > 0); + if (--handler->refcount > 0) + return; + + i_assert(hash_table_count(handler->requests) == 0); + + /* notify parent that we're done with all requests */ + handler->callback(NULL, handler->conn); + + hash_table_destroy(&handler->requests); + pool_unref(&handler->pool); +} + +void auth_request_handler_destroy(struct auth_request_handler **_handler) +{ + struct auth_request_handler *handler = *_handler; + + *_handler = NULL; + + i_assert(!handler->destroyed); + + handler->destroyed = TRUE; + auth_request_handler_unref(&handler); +} + +void auth_request_handler_set(struct auth_request_handler *handler, + unsigned int connect_uid, + unsigned int client_pid) +{ + handler->connect_uid = connect_uid; + handler->client_pid = client_pid; +} + +static void auth_request_handler_remove(struct auth_request_handler *handler, + struct auth_request *request) +{ + i_assert(request->handler == handler); + + if (request->removed_from_handler) { + /* already removed it */ + return; + } + request->removed_from_handler = TRUE; + + /* if db lookup is stuck, this call doesn't actually free the auth + request, so make sure we don't get back here. */ + timeout_remove(&request->to_abort); + + hash_table_remove(handler->requests, POINTER_CAST(request->id)); + auth_request_unref(&request); +} + +static void +auth_str_add_keyvalue(string_t *dest, const char *key, const char *value) +{ + str_append_c(dest, '\t'); + str_append(dest, key); + str_append_c(dest, '='); + str_append_tabescaped(dest, value); +} + +static void +auth_str_append_extra_fields(struct auth_request *request, string_t *dest) +{ + const struct auth_request_fields *fields = &request->fields; + + if (!auth_fields_is_empty(fields->extra_fields)) { + str_append_c(dest, '\t'); + auth_fields_append(fields->extra_fields, dest, + AUTH_FIELD_FLAG_HIDDEN, 0); + } + + if (fields->original_username != NULL && + null_strcmp(fields->original_username, fields->user) != 0 && + !auth_fields_exists(fields->extra_fields, "original_user")) { + auth_str_add_keyvalue(dest, "original_user", + fields->original_username); + } + if (fields->master_user != NULL && + !auth_fields_exists(fields->extra_fields, "auth_user")) + auth_str_add_keyvalue(dest, "auth_user", fields->master_user); + if (*request->set->anonymous_username != '\0' && + null_strcmp(fields->user, request->set->anonymous_username) == 0) { + /* this is an anonymous login, either via ANONYMOUS + SASL mechanism or simply logging in as the anonymous + user via another mechanism */ + str_append(dest, "\tanonymous"); + } + if (!request->auth_only && + auth_fields_exists(fields->extra_fields, "proxy")) { + /* we're proxying */ + if (!auth_fields_exists(fields->extra_fields, "pass") && + request->mech_password != NULL) { + /* send back the password that was sent by user + (not the password in passdb). */ + auth_str_add_keyvalue(dest, "pass", + request->mech_password); + } + if (fields->master_user != NULL && + !auth_fields_exists(fields->extra_fields, "master") && + *fields->master_user != '\0') { + /* the master username needs to be forwarded */ + auth_str_add_keyvalue(dest, "master", + fields->master_user); + } + } +} + +static void +auth_request_handle_failure(struct auth_request *request, const char *reply) +{ + struct auth_request_handler *handler = request->handler; + + /* handle failure here */ + auth_request_log_finished(request); + + if (request->in_delayed_failure_queue) { + /* we came here from flush_failures() */ + handler->callback(reply, handler->conn); + return; + } + + /* remove the request from requests-list */ + auth_request_ref(request); + auth_request_handler_remove(handler, request); + + if (request->set->policy_report_after_auth) + auth_policy_report(request); + + if (auth_fields_exists(request->fields.extra_fields, "nodelay")) { + /* passdb specifically requested not to delay the reply. */ + handler->callback(reply, handler->conn); + auth_request_unref(&request); + return; + } + + /* failure. don't announce it immediately to avoid + a) timing attacks, b) flooding */ + request->in_delayed_failure_queue = TRUE; + handler->refcount++; + + if (auth_penalty != NULL) { + auth_penalty_update(auth_penalty, request, + request->last_penalty + 1); + } + + auth_request_refresh_last_access(request); + aqueue_append(auth_failures, &request); + if (to_auth_failures == NULL) { + to_auth_failures = + timeout_add_short(AUTH_FAILURE_DELAY_CHECK_MSECS, + auth_failure_timeout, NULL); + } +} + +static void +auth_request_handler_reply_success_finish(struct auth_request *request) +{ + struct auth_request_handler *handler = request->handler; + string_t *str = t_str_new(128); + + auth_request_log_finished(request); + + if (request->last_penalty != 0 && auth_penalty != NULL) { + /* reset penalty */ + auth_penalty_update(auth_penalty, request, 0); + } + + /* sanitize these fields, since the login code currently assumes they + are exactly in this format. */ + auth_fields_booleanize(request->fields.extra_fields, "nologin"); + auth_fields_booleanize(request->fields.extra_fields, "proxy"); + + str_printfa(str, "OK\t%u\tuser=", request->id); + str_append_tabescaped(str, request->fields.user); + auth_str_append_extra_fields(request, str); + + if (request->set->policy_report_after_auth) + auth_policy_report(request); + + if (handler->master_callback == NULL || + auth_fields_exists(request->fields.extra_fields, "nologin") || + auth_fields_exists(request->fields.extra_fields, "proxy")) { + /* this request doesn't have to wait for master + process to pick it up. delete it */ + auth_request_handler_remove(handler, request); + } + + handler->callback(str_c(str), handler->conn); +} + +static void +auth_request_handler_reply_failure_finish(struct auth_request *request) +{ + const char *code = NULL; + string_t *str = t_str_new(128); + + auth_fields_remove(request->fields.extra_fields, "nologin"); + + str_printfa(str, "FAIL\t%u", request->id); + if (request->fields.user != NULL) + auth_str_add_keyvalue(str, "user", request->fields.user); + else if (request->fields.original_username != NULL) { + auth_str_add_keyvalue(str, "user", + request->fields.original_username); + } + + if (request->internal_failure) { + code = AUTH_CLIENT_FAIL_CODE_TEMPFAIL; + } else if (request->fields.master_user != NULL) { + /* authentication succeeded, but we can't log in + as the wanted user */ + code = AUTH_CLIENT_FAIL_CODE_AUTHZFAILED; + } else { + switch (request->passdb_result) { + case PASSDB_RESULT_NEXT: + case PASSDB_RESULT_INTERNAL_FAILURE: + case PASSDB_RESULT_SCHEME_NOT_AVAILABLE: + case PASSDB_RESULT_USER_UNKNOWN: + case PASSDB_RESULT_PASSWORD_MISMATCH: + case PASSDB_RESULT_OK: + break; + case PASSDB_RESULT_USER_DISABLED: + code = AUTH_CLIENT_FAIL_CODE_USER_DISABLED; + break; + case PASSDB_RESULT_PASS_EXPIRED: + code = AUTH_CLIENT_FAIL_CODE_PASS_EXPIRED; + break; + } + } + + if (auth_fields_exists(request->fields.extra_fields, "nodelay")) { + /* this is normally a hidden field, need to add it explicitly */ + str_append(str, "\tnodelay"); + } + + if (code != NULL) { + str_append(str, "\tcode="); + str_append(str, code); + } + auth_str_append_extra_fields(request, str); + + auth_request_handle_failure(request, str_c(str)); +} + +static void +auth_request_handler_proxy_callback(bool success, struct auth_request *request) +{ + struct auth_request_handler *handler = request->handler; + + if (success) + auth_request_handler_reply_success_finish(request); + else + auth_request_handler_reply_failure_finish(request); + auth_request_handler_unref(&handler); +} + +void auth_request_handler_reply(struct auth_request *request, + enum auth_client_result result, + const void *auth_reply, size_t reply_size) +{ + struct auth_request_handler *handler = request->handler; + + request->handler_pending_reply = FALSE; + handler->reply_callback(request, result, auth_reply, reply_size); +} + +static void +auth_request_handler_default_reply_callback(struct auth_request *request, + enum auth_client_result result, + const void *auth_reply, + size_t reply_size) +{ + struct auth_request_handler *handler = request->handler; + string_t *str; + int ret; + + if (handler->destroyed) { + /* the client connection was already closed. we can't do + anything but abort this request */ + request->internal_failure = TRUE; + result = AUTH_CLIENT_RESULT_FAILURE; + /* make sure this request is set to finished state + (it's not with result=continue) */ + auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED); + } + + switch (result) { + case AUTH_CLIENT_RESULT_CONTINUE: + str = t_str_new(16 + MAX_BASE64_ENCODED_SIZE(reply_size)); + str_printfa(str, "CONT\t%u\t", request->id); + base64_encode(auth_reply, reply_size, str); + + request->accept_cont_input = TRUE; + handler->callback(str_c(str), handler->conn); + break; + case AUTH_CLIENT_RESULT_SUCCESS: + if (reply_size > 0) { + str = t_str_new(MAX_BASE64_ENCODED_SIZE(reply_size)); + base64_encode(auth_reply, reply_size, str); + auth_fields_add(request->fields.extra_fields, "resp", + str_c(str), 0); + } + ret = auth_request_proxy_finish(request, + auth_request_handler_proxy_callback); + if (ret < 0) + auth_request_handler_reply_failure_finish(request); + else if (ret > 0) + auth_request_handler_reply_success_finish(request); + else + return; + break; + case AUTH_CLIENT_RESULT_FAILURE: + auth_request_proxy_finish_failure(request); + auth_request_handler_reply_failure_finish(request); + break; + } + /* NOTE: request may be destroyed now */ + + auth_request_handler_unref(&handler); +} + +void auth_request_handler_reply_continue(struct auth_request *request, + const void *reply, size_t reply_size) +{ + request->handler->reply_continue_callback(request, reply, reply_size); +} + +static void +auth_request_handler_default_reply_continue(struct auth_request *request, + const void *reply, + size_t reply_size) +{ + auth_request_handler_reply(request, AUTH_CLIENT_RESULT_CONTINUE, + reply, reply_size); +} + +void auth_request_handler_abort(struct auth_request *request) +{ + i_assert(request->handler_pending_reply); + + /* request destroyed while waiting for auth_request_penalty_finish() + to be called. */ + auth_request_handler_unref(&request->handler); +} + +static void +auth_request_handler_auth_fail_code(struct auth_request_handler *handler, + struct auth_request *request, + const char *fail_code, const char *reason) +{ + string_t *str = t_str_new(128); + + e_info(request->mech_event, "%s", reason); + + str_printfa(str, "FAIL\t%u", request->id); + if (*fail_code != '\0') { + str_append(str, "\tcode="); + str_append(str, fail_code); + } + str_append(str, "\treason="); + str_append_tabescaped(str, reason); + + handler->callback(str_c(str), handler->conn); + auth_request_handler_remove(handler, request); +} + +static void auth_request_handler_auth_fail +(struct auth_request_handler *handler, struct auth_request *request, + const char *reason) +{ + auth_request_handler_auth_fail_code(handler, request, "", reason); +} + +static void auth_request_timeout(struct auth_request *request) +{ + unsigned int secs = (unsigned int)(time(NULL) - request->last_access); + + if (request->state != AUTH_REQUEST_STATE_MECH_CONTINUE) { + /* client's fault */ + e_error(request->mech_event, + "Request %u.%u timed out after %u secs, state=%d", + request->handler->client_pid, request->id, + secs, request->state); + } else if (request->set->verbose) { + e_info(request->mech_event, + "Request timed out waiting for client to continue authentication " + "(%u secs)", secs); + } + auth_request_handler_remove(request->handler, request); +} + +static void auth_request_penalty_finish(struct auth_request *request) +{ + timeout_remove(&request->to_penalty); + auth_request_initial(request); +} + +static void +auth_penalty_callback(unsigned int penalty, struct auth_request *request) +{ + unsigned int secs; + + request->last_penalty = penalty; + + if (penalty == 0) + auth_request_initial(request); + else { + secs = auth_penalty_to_secs(penalty); + request->to_penalty = timeout_add(secs * 1000, + auth_request_penalty_finish, + request); + } +} + +bool auth_request_handler_auth_begin(struct auth_request_handler *handler, + const char *args) +{ + const struct mech_module *mech; + struct auth_request *request; + const char *const *list, *name, *arg, *initial_resp; + void *initial_resp_data; + unsigned int id; + buffer_t *buf; + + i_assert(!handler->destroyed); + + /* <id> <mechanism> [...] */ + list = t_strsplit_tabescaped(args); + if (list[0] == NULL || list[1] == NULL || + str_to_uint(list[0], &id) < 0 || id == 0) { + e_error(handler->conn->event, + "BUG: Authentication client %u " + "sent broken AUTH request", handler->client_pid); + return FALSE; + } + + if (handler->token_auth) { + mech = &mech_dovecot_token; + if (strcmp(list[1], mech->mech_name) != 0) { + /* unsupported mechanism */ + e_error(handler->conn->event, + "BUG: Authentication client %u requested invalid " + "authentication mechanism %s (DOVECOT-TOKEN required)", + handler->client_pid, str_sanitize(list[1], MAX_MECH_NAME_LEN)); + return FALSE; + } + } else { + struct auth *auth_default = auth_default_service(); + mech = mech_register_find(auth_default->reg, list[1]); + if (mech == NULL) { + /* unsupported mechanism */ + e_error(handler->conn->event, + "BUG: Authentication client %u requested unsupported " + "authentication mechanism %s", handler->client_pid, + str_sanitize(list[1], MAX_MECH_NAME_LEN)); + return FALSE; + } + } + + request = auth_request_new(mech, handler->conn->event); + request->handler = handler; + request->connect_uid = handler->connect_uid; + request->client_pid = handler->client_pid; + request->id = id; + request->auth_only = handler->master_callback == NULL; + + /* parse optional parameters */ + initial_resp = NULL; + for (list += 2; *list != NULL; list++) { + arg = strchr(*list, '='); + if (arg == NULL) { + name = *list; + arg = ""; + } else { + name = t_strdup_until(*list, arg); + arg++; + } + + if (auth_request_import_auth(request, name, arg)) + ; + else if (strcmp(name, "resp") == 0) { + initial_resp = arg; + /* this must be the last parameter */ + list++; + break; + } + } + + if (*list != NULL) { + e_error(handler->conn->event, + "BUG: Authentication client %u " + "sent AUTH parameters after 'resp'", + handler->client_pid); + auth_request_unref(&request); + return FALSE; + } + + if (request->fields.service == NULL) { + e_error(handler->conn->event, + "BUG: Authentication client %u " + "didn't specify service in request", + handler->client_pid); + auth_request_unref(&request); + return FALSE; + } + if (hash_table_lookup(handler->requests, POINTER_CAST(id)) != NULL) { + e_error(handler->conn->event, + "BUG: Authentication client %u " + "sent a duplicate ID %u", handler->client_pid, id); + auth_request_unref(&request); + return FALSE; + } + auth_request_init(request); + + request->to_abort = timeout_add(MASTER_AUTH_SERVER_TIMEOUT_SECS * 1000, + auth_request_timeout, request); + hash_table_insert(handler->requests, POINTER_CAST(id), request); + + if (request->set->ssl_require_client_cert && + !request->fields.valid_client_cert) { + /* we fail without valid certificate */ + auth_request_handler_auth_fail(handler, request, + "Client didn't present valid SSL certificate"); + return TRUE; + } + + if (request->set->ssl_require_client_cert && + request->set->ssl_username_from_cert && + !request->fields.cert_username) { + auth_request_handler_auth_fail(handler, request, + "SSL certificate didn't contain username"); + return TRUE; + } + + /* Handle initial respose */ + if (initial_resp == NULL) { + /* No initial response */ + request->initial_response = NULL; + request->initial_response_len = 0; + } else if (handler->conn->version_minor < 2 && *initial_resp == '\0') { + /* Some authentication clients like Exim send and empty initial + response field when it is in fact absent in the + authentication command. This was allowed for older versions + of the Dovecot authentication protocol. */ + request->initial_response = NULL; + request->initial_response_len = 0; + } else if (*initial_resp == '\0' || strcmp(initial_resp, "=") == 0 ) { + /* Empty initial response - Protocols that use SASL often + use '=' to indicate an empty initial response; i.e., to + distinguish it from an absent initial response. However, that + should not be conveyed to the SASL layer (it is not even + valid Base64); only the empty string should be passed on. + Still, we recognize it here anyway, because we used to make + the same mistake. */ + request->initial_response = uchar_empty_ptr; + request->initial_response_len = 0; + } else { + size_t len = strlen(initial_resp); + + /* Initial response encoded in Bas64 */ + buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(len)); + if (base64_decode(initial_resp, len, NULL, buf) < 0) { + auth_request_handler_auth_fail_code(handler, request, + AUTH_CLIENT_FAIL_CODE_INVALID_BASE64, + "Invalid base64 data in initial response"); + return TRUE; + } + initial_resp_data = + p_malloc(request->pool, I_MAX(buf->used, 1)); + memcpy(initial_resp_data, buf->data, buf->used); + request->initial_response = initial_resp_data; + request->initial_response_len = buf->used; + } + + /* handler is referenced until auth_request_handler_reply() + is called. */ + handler->refcount++; + request->handler_pending_reply = TRUE; + + /* before we start authenticating, see if we need to wait first */ + auth_penalty_lookup(auth_penalty, request, auth_penalty_callback); + return TRUE; +} + +bool auth_request_handler_auth_continue(struct auth_request_handler *handler, + const char *args) +{ + struct auth_request *request; + const char *data; + size_t data_len; + buffer_t *buf; + unsigned int id; + + data = strchr(args, '\t'); + if (data == NULL || str_to_uint(t_strdup_until(args, data), &id) < 0) { + e_error(handler->conn->event, + "BUG: Authentication client sent broken CONT request"); + return FALSE; + } + data++; + + request = hash_table_lookup(handler->requests, POINTER_CAST(id)); + if (request == NULL) { + const char *reply = t_strdup_printf( + "FAIL\t%u\treason=Authentication request timed out", id); + handler->callback(reply, handler->conn); + return TRUE; + } + + /* accept input only once after mechanism has sent a CONT reply */ + if (!request->accept_cont_input) { + auth_request_handler_auth_fail(handler, request, + "Unexpected continuation"); + return TRUE; + } + request->accept_cont_input = FALSE; + + data_len = strlen(data); + buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(data_len)); + if (base64_decode(data, data_len, NULL, buf) < 0) { + auth_request_handler_auth_fail_code(handler, request, + AUTH_CLIENT_FAIL_CODE_INVALID_BASE64, + "Invalid base64 data in continued response"); + return TRUE; + } + + /* handler is referenced until auth_request_handler_reply() + is called. */ + handler->refcount++; + auth_request_continue(request, buf->data, buf->used); + return TRUE; +} + +static void auth_str_append_userdb_extra_fields(struct auth_request *request, + string_t *dest) +{ + str_append_c(dest, '\t'); + auth_fields_append(request->fields.userdb_reply, dest, + AUTH_FIELD_FLAG_HIDDEN, 0); + + if (request->fields.master_user != NULL && + !auth_fields_exists(request->fields.userdb_reply, "master_user")) { + auth_str_add_keyvalue(dest, "master_user", + request->fields.master_user); + } + auth_str_add_keyvalue(dest, "auth_mech", request->mech->mech_name); + if (*request->set->anonymous_username != '\0' && + strcmp(request->fields.user, request->set->anonymous_username) == 0) { + /* this is an anonymous login, either via ANONYMOUS + SASL mechanism or simply logging in as the anonymous + user via another mechanism */ + str_append(dest, "\tanonymous"); + } + /* generate auth_token when master service provided session_pid */ + if (request->request_auth_token && + request->session_pid != (pid_t)-1) { + const char *auth_token = + auth_token_get(request->fields.service, + dec2str(request->session_pid), + request->fields.user, + request->fields.session_id); + auth_str_add_keyvalue(dest, "auth_token", auth_token); + } + if (request->fields.master_user != NULL) { + auth_str_add_keyvalue(dest, "auth_user", + request->fields.master_user); + } else if (request->fields.original_username != NULL && + strcmp(request->fields.original_username, + request->fields.user) != 0) { + auth_str_add_keyvalue(dest, "auth_user", + request->fields.original_username); + } +} + +static void userdb_callback(enum userdb_result result, + struct auth_request *request) +{ + struct auth_request_handler *handler = request->handler; + string_t *str; + const char *value; + + i_assert(request->state == AUTH_REQUEST_STATE_USERDB); + + auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED); + + if (request->userdb_lookup_tempfailed) + result = USERDB_RESULT_INTERNAL_FAILURE; + + str = t_str_new(128); + switch (result) { + case USERDB_RESULT_INTERNAL_FAILURE: + str_printfa(str, "FAIL\t%u", request->id); + if (request->userdb_lookup_tempfailed) { + value = auth_fields_find(request->fields.userdb_reply, + "reason"); + if (value != NULL) + auth_str_add_keyvalue(str, "reason", value); + } + break; + case USERDB_RESULT_USER_UNKNOWN: + str_printfa(str, "NOTFOUND\t%u", request->id); + break; + case USERDB_RESULT_OK: + str_printfa(str, "USER\t%u\t", request->id); + str_append_tabescaped(str, request->fields.user); + auth_str_append_userdb_extra_fields(request, str); + break; + } + handler->master_callback(str_c(str), request->master); + + auth_master_connection_unref(&request->master); + auth_request_unref(&request); + auth_request_handler_unref(&handler); +} + +static bool +auth_master_request_failed(struct auth_request_handler *handler, + struct auth_master_connection *master, + unsigned int id) +{ + if (handler->master_callback == NULL) + return FALSE; + handler->master_callback(t_strdup_printf("FAIL\t%u", id), master); + return TRUE; +} + +bool auth_request_handler_master_request(struct auth_request_handler *handler, + struct auth_master_connection *master, + unsigned int id, unsigned int client_id, + const char *const *params) +{ + struct auth_request *request; + struct net_unix_cred cred; + + request = hash_table_lookup(handler->requests, POINTER_CAST(client_id)); + if (request == NULL) { + e_error(master->event, "Master request %u.%u not found", + handler->client_pid, client_id); + return auth_master_request_failed(handler, master, id); + } + + auth_request_ref(request); + auth_request_handler_remove(handler, request); + + for (; *params != NULL; params++) { + const char *name, *param = strchr(*params, '='); + + if (param == NULL) { + name = *params; + param = ""; + } else { + name = t_strdup_until(*params, param); + param++; + } + + (void)auth_request_import_master(request, name, param); + } + + /* verify session pid if specified and possible */ + if (request->session_pid != (pid_t)-1 && + net_getunixcred(master->fd, &cred) == 0 && + cred.pid != (pid_t)-1 && request->session_pid != cred.pid) { + e_error(master->event, + "Session pid %ld provided by master for request %u.%u " + "did not match peer credentials (pid=%ld, uid=%ld)", + (long)request->session_pid, + handler->client_pid, client_id, + (long)cred.pid, (long)cred.uid); + return auth_master_request_failed(handler, master, id); + } + + if (request->state != AUTH_REQUEST_STATE_FINISHED || + !request->fields.successful) { + e_error(master->event, + "Master requested unfinished authentication request " + "%u.%u", handler->client_pid, client_id); + handler->master_callback(t_strdup_printf("FAIL\t%u", id), + master); + auth_request_unref(&request); + } else { + /* the request isn't being referenced anywhere anymore, + so we can do a bit of kludging.. replace the request's + old client_id with master's id. */ + auth_request_set_state(request, AUTH_REQUEST_STATE_USERDB); + request->id = id; + request->master = master; + + /* master and handler are referenced until userdb_callback i + s called. */ + auth_master_connection_ref(master); + handler->refcount++; + auth_request_lookup_user(request, userdb_callback); + } + return TRUE; +} + +void auth_request_handler_cancel_request(struct auth_request_handler *handler, + unsigned int client_id) +{ + struct auth_request *request; + + request = hash_table_lookup(handler->requests, POINTER_CAST(client_id)); + if (request != NULL) + auth_request_handler_remove(handler, request); +} + +void auth_request_handler_flush_failures(bool flush_all) +{ + struct auth_request **auth_requests, *auth_request; + unsigned int i, j, count; + time_t diff; + + count = aqueue_count(auth_failures); + if (count == 0) { + timeout_remove(&to_auth_failures); + return; + } + + auth_requests = array_front_modifiable(&auth_failures_arr); + /* count the number of requests that we need to flush */ + for (i = 0; i < count; i++) { + auth_request = auth_requests[aqueue_idx(auth_failures, i)]; + + /* FIXME: assumes that failure_delay is always the same. */ + diff = ioloop_time - auth_request->last_access; + if (diff < (time_t)auth_request->set->failure_delay && + !flush_all) + break; + } + + /* shuffle these requests to try to prevent any kind of timing attacks + where attacker performs multiple requests in parallel and attempts + to figure out results based on the order of replies. */ + count = i; + for (i = 0; i < count; i++) { + j = random() % (count - i) + i; + auth_request = auth_requests[aqueue_idx(auth_failures, i)]; + + /* swap i & j */ + auth_requests[aqueue_idx(auth_failures, i)] = + auth_requests[aqueue_idx(auth_failures, j)]; + auth_requests[aqueue_idx(auth_failures, j)] = auth_request; + } + + /* flush the requests */ + for (i = 0; i < count; i++) { + auth_request = auth_requests[aqueue_idx(auth_failures, 0)]; + aqueue_delete_tail(auth_failures); + + i_assert(auth_request != NULL); + i_assert(auth_request->state == AUTH_REQUEST_STATE_FINISHED); + auth_request_handler_reply(auth_request, + AUTH_CLIENT_RESULT_FAILURE, + uchar_empty_ptr, 0); + auth_request_unref(&auth_request); + } +} + +static void auth_failure_timeout(void *context ATTR_UNUSED) +{ + auth_request_handler_flush_failures(FALSE); +} + +void auth_request_handler_init(void) +{ + i_array_init(&auth_failures_arr, 128); + auth_failures = aqueue_init(&auth_failures_arr.arr); +} + +void auth_request_handler_deinit(void) +{ + auth_request_handler_flush_failures(TRUE); + array_free(&auth_failures_arr); + aqueue_deinit(&auth_failures); + + timeout_remove(&to_auth_failures); +} |