imap-proxy.c revision 2c5c293940fd6c7e020e1d58dae77a9d01f9059b
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen/* Copyright (c) 2004-2016 Dovecot authors, see the included COPYING file */
1d4f710106fb498750456724628da6063e012e6dTimo Sirainen
1d4f710106fb498750456724628da6063e012e6dTimo Sirainen#include "login-common.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "array.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "ioloop.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "istream.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "ostream.h"
1d4f710106fb498750456724628da6063e012e6dTimo Sirainen#include "base64.h"
1d4f710106fb498750456724628da6063e012e6dTimo Sirainen#include "str.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "str-sanitize.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "safe-memset.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "dsasl-client.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "imap-login-client.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "client-authenticate.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "imap-resp-code.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "imap-quote.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen#include "imap-proxy.h"
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainenenum imap_proxy_state {
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen IMAP_PROXY_STATE_NONE,
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen IMAP_PROXY_STATE_BANNER,
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen IMAP_PROXY_STATE_ID,
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen IMAP_PROXY_STATE_STARTTLS,
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen IMAP_PROXY_STATE_CAPABILITY,
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen IMAP_PROXY_STATE_AUTH_CONTINUE,
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen IMAP_PROXY_STATE_LOGIN
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen};
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainenstatic void proxy_write_id(struct imap_client *client, string_t *str)
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen{
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen i_assert(client->common.proxy_ttl > 1);
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen str_printfa(str, "I ID ("
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen "\"x-session-id\" \"%s\" "
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen "\"x-originating-ip\" \"%s\" "
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen "\"x-originating-port\" \"%u\" "
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen "\"x-connected-ip\" \"%s\" "
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen "\"x-connected-port\" \"%u\" "
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen "\"x-proxy-ttl\" \"%u\")\r\n",
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen client_get_session_id(&client->common),
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen net_ip2addr(&client->common.ip),
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen client->common.remote_port,
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen net_ip2addr(&client->common.local_ip),
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen client->common.local_port,
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen client->common.proxy_ttl - 1);
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen}
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainenstatic void proxy_free_password(struct client *client)
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen{
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen if (client->proxy_password == NULL)
1d4f710106fb498750456724628da6063e012e6dTimo Sirainen return;
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen safe_memset(client->proxy_password, 0, strlen(client->proxy_password));
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen i_free_and_null(client->proxy_password);
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen}
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainenstatic int proxy_write_starttls(struct imap_client *client, string_t *str)
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen{
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen enum login_proxy_ssl_flags ssl_flags = login_proxy_get_ssl_flags(client->common.login_proxy);
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen if ((ssl_flags & PROXY_SSL_FLAG_STARTTLS) != 0) {
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen if (client->proxy_backend_capability != NULL &&
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen !str_array_icase_find(t_strsplit(client->proxy_backend_capability, " "), "STARTTLS")) {
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen client_log_err(&client->common,
1d4f710106fb498750456724628da6063e012e6dTimo Sirainen "proxy: Remote doesn't support STARTTLS");
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen return -1;
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen }
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen str_append(str, "S STARTTLS\r\n");
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen return 1;
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen }
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen return 0;
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen}
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainenstatic int proxy_write_login(struct imap_client *client, string_t *str)
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen{
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen struct dsasl_client_settings sasl_set;
1d4f710106fb498750456724628da6063e012e6dTimo Sirainen const unsigned char *output;
1d4f710106fb498750456724628da6063e012e6dTimo Sirainen size_t len;
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen const char *mech_name, *error;
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen /* Send CAPABILITY command if we don't know the capabilities yet.
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen Also as kind of a Dovecot-backend workaround if the client insisted
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen on sending CAPABILITY command (even though our banner already sent
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen it), send the (unnecessary) CAPABILITY command to backend as well
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen to avoid sending the CAPABILITY reply twice (untagged and OK resp
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen code). */
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen if (!client->proxy_capability_request_sent &&
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen (client->proxy_backend_capability == NULL ||
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen client->client_ignores_capability_resp_code)) {
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen client->proxy_capability_request_sent = TRUE;
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen str_append(str, "C CAPABILITY\r\n");
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen if (client->common.proxy_nopipelining) {
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen /* authenticate only after receiving C OK reply. */
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen return 0;
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen }
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen }
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen if (client->common.proxy_mech == NULL) {
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen /* logging in normally - use LOGIN command */
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen if (client->proxy_logindisabled &&
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen login_proxy_get_ssl_flags(client->common.login_proxy) == 0) {
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen client_log_err(&client->common,
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen "proxy: Remote advertised LOGINDISABLED and SSL/TLS not enabled");
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen return -1;
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen }
9439bed2f07d6475febd8a247cd2f0990fb32a13Timo Sirainen str_append(str, "L LOGIN ");
1d4f710106fb498750456724628da6063e012e6dTimo Sirainen imap_append_string(str, client->common.proxy_user);
1d4f710106fb498750456724628da6063e012e6dTimo Sirainen str_append_c(str, ' ');
imap_append_string(str, client->common.proxy_password);
str_append(str, "\r\n");
proxy_free_password(&client->common);
return 0;
}
i_assert(client->common.proxy_sasl_client == NULL);
memset(&sasl_set, 0, sizeof(sasl_set));
sasl_set.authid = client->common.proxy_master_user != NULL ?
client->common.proxy_master_user : client->common.proxy_user;
sasl_set.authzid = client->common.proxy_user;
sasl_set.password = client->common.proxy_password;
client->common.proxy_sasl_client =
dsasl_client_new(client->common.proxy_mech, &sasl_set);
mech_name = dsasl_client_mech_get_name(client->common.proxy_mech);
str_append(str, "L AUTHENTICATE ");
str_append(str, mech_name);
if (client->proxy_sasl_ir) {
if (dsasl_client_output(client->common.proxy_sasl_client,
&output, &len, &error) < 0) {
client_log_err(&client->common, t_strdup_printf(
"proxy: SASL mechanism %s init failed: %s",
mech_name, error));
return -1;
}
str_append_c(str, ' ');
if (len == 0)
str_append_c(str, '=');
else
base64_encode(output, len, str);
}
str_append(str, "\r\n");
proxy_free_password(&client->common);
return 0;
}
static int proxy_input_banner(struct imap_client *client,
struct ostream *output, const char *line)
{
const char *const *capabilities = NULL;
string_t *str;
int ret;
if (strncmp(line, "* OK ", 5) != 0) {
client_log_err(&client->common, t_strdup_printf(
"proxy: Remote returned invalid banner: %s",
str_sanitize(line, 160)));
return -1;
}
str = t_str_new(128);
if (strncmp(line + 5, "[CAPABILITY ", 12) == 0) {
capabilities = t_strsplit(t_strcut(line + 5 + 12, ']'), " ");
if (str_array_icase_find(capabilities, "SASL-IR"))
client->proxy_sasl_ir = TRUE;
if (str_array_icase_find(capabilities, "LOGINDISABLED"))
client->proxy_logindisabled = TRUE;
i_free(client->proxy_backend_capability);
client->proxy_backend_capability =
i_strdup(t_strcut(line + 5 + 12, ']'));
if (str_array_icase_find(capabilities, "ID") &&
!client->common.proxy_not_trusted) {
proxy_write_id(client, str);
if (client->common.proxy_nopipelining) {
/* write login or starttls after I OK */
o_stream_nsend(output, str_data(str), str_len(str));
return 0;
}
}
}
if ((ret = proxy_write_starttls(client, str)) < 0) {
return -1;
} else if (ret == 0) {
if (proxy_write_login(client, str) < 0)
return -1;
}
o_stream_nsend(output, str_data(str), str_len(str));
return 0;
}
static void
client_send_login_reply(struct imap_client *client, string_t *str,
const char *line)
{
const char *capability;
bool tagged_capability;
capability = client->proxy_backend_capability;
tagged_capability = strncasecmp(line, "[CAPABILITY ", 12) == 0;
if (tagged_capability)
capability = t_strcut(line + 12, ']');
if (client->client_ignores_capability_resp_code && capability != NULL) {
/* client has used CAPABILITY command, so it didn't understand
the capabilities in the banner. send the backend's untagged
CAPABILITY reply and hope that the client understands it */
str_printfa(str, "* CAPABILITY %s\r\n", capability);
}
str_append(str, client->cmd_tag);
str_append(str, " OK ");
if (!client->client_ignores_capability_resp_code &&
!tagged_capability && capability != NULL) {
str_printfa(str, "[CAPABILITY %s] ", capability);
if (*line == '[') {
/* we need to send the capability.
skip over this resp-code */
while (*line != ']' && *line != '\0')
line++;
if (*line == ' ') line++;
}
}
str_append(str, line);
str_append(str, "\r\n");
}
int imap_proxy_parse_line(struct client *client, const char *line)
{
struct imap_client *imap_client = (struct imap_client *)client;
struct ostream *output;
string_t *str;
const unsigned char *data;
size_t data_len;
const char *error;
int ret;
i_assert(!client->destroyed);
output = login_proxy_get_ostream(client->login_proxy);
if (!imap_client->proxy_seen_banner) {
/* this is a banner */
client->proxy_state = IMAP_PROXY_STATE_BANNER;
imap_client->proxy_seen_banner = TRUE;
if (proxy_input_banner(imap_client, output, line) < 0) {
client_proxy_failed(client, TRUE);
return -1;
}
return 0;
} else if (*line == '+') {
/* AUTHENTICATE started. finish it. */
if (client->proxy_sasl_client == NULL) {
/* used literals with LOGIN command, just ignore. */
return 0;
}
client->proxy_state = IMAP_PROXY_STATE_AUTH_CONTINUE;
str = t_str_new(128);
if (line[1] != ' ' ||
base64_decode(line+2, strlen(line+2), NULL, str) < 0) {
client_log_err(client,
"proxy: Server sent invalid base64 data in AUTHENTICATE response");
client_proxy_failed(client, TRUE);
return -1;
}
ret = dsasl_client_input(client->proxy_sasl_client,
str_data(str), str_len(str), &error);
if (ret == 0) {
ret = dsasl_client_output(client->proxy_sasl_client,
&data, &data_len, &error);
}
if (ret < 0) {
client_log_err(client, t_strdup_printf(
"proxy: Server sent invalid authentication data: %s",
error));
client_proxy_failed(client, TRUE);
return -1;
}
i_assert(ret == 0);
str_truncate(str, 0);
base64_encode(data, data_len, str);
str_append(str, "\r\n");
o_stream_nsend(output, str_data(str), str_len(str));
return 0;
} else if (strncmp(line, "S ", 2) == 0) {
if (strncmp(line, "S OK ", 5) != 0) {
/* STARTTLS failed */
client_log_err(client, t_strdup_printf(
"proxy: Remote STARTTLS failed: %s",
str_sanitize(line + 5, 160)));
client_proxy_failed(client, TRUE);
return -1;
}
/* STARTTLS successful, begin TLS negotiation. */
client->proxy_state = IMAP_PROXY_STATE_STARTTLS;
if (login_proxy_starttls(client->login_proxy) < 0) {
client_proxy_failed(client, TRUE);
return -1;
}
/* i/ostreams changed. */
output = login_proxy_get_ostream(client->login_proxy);
str = t_str_new(128);
if (proxy_write_login(imap_client, str) < 0) {
client_proxy_failed(client, TRUE);
return -1;
}
o_stream_nsend(output, str_data(str), str_len(str));
return 1;
} else if (strncmp(line, "L OK ", 5) == 0) {
/* Login successful. Send this line to client. */
client->proxy_state = IMAP_PROXY_STATE_LOGIN;
str = t_str_new(128);
client_send_login_reply(imap_client, str, line + 5);
o_stream_nsend(client->output, str_data(str), str_len(str));
(void)client_skip_line(imap_client);
client_proxy_finish_destroy_client(client);
return 1;
} else if (strncmp(line, "L ", 2) == 0) {
line += 2;
if (client->set->auth_verbose) {
const char *log_line = line;
if (strncasecmp(log_line, "NO ", 3) == 0)
log_line += 3;
client_proxy_log_failure(client, log_line);
}
#define STR_NO_IMAP_RESP_CODE_AUTHFAILED "NO ["IMAP_RESP_CODE_AUTHFAILED"]"
if (strncmp(line, STR_NO_IMAP_RESP_CODE_AUTHFAILED,
strlen(STR_NO_IMAP_RESP_CODE_AUTHFAILED)) == 0) {
/* the remote sent a generic "authentication failed"
error. replace it with our one, so that in case
the remote is sending a different error message
an attacker can't find out what users exist in
the system. */
client_send_reply_code(client, IMAP_CMD_REPLY_NO,
IMAP_RESP_CODE_AUTHFAILED,
AUTH_FAILED_MSG);
} else if (strncmp(line, "NO [", 4) == 0) {
/* remote sent some other resp-code. forward it. */
client_send_raw(client, t_strconcat(
imap_client->cmd_tag, " ", line, "\r\n", NULL));
} else {
/* there was no [resp-code], so remote isn't Dovecot
v1.2+. we could either forward the line as-is and
leak information about what users exist in this
system, or we could hide other errors than password
failures. since other errors are pretty rare,
it's safer to just hide them. they're still
available in logs though. */
client_send_reply_code(client, IMAP_CMD_REPLY_NO,
IMAP_RESP_CODE_AUTHFAILED,
AUTH_FAILED_MSG);
}
client->proxy_auth_failed = TRUE;
client_proxy_failed(client, FALSE);
return -1;
} else if (strncasecmp(line, "* CAPABILITY ", 13) == 0) {
i_free(imap_client->proxy_backend_capability);
imap_client->proxy_backend_capability = i_strdup(line + 13);
return 0;
} else if (strncmp(line, "C ", 2) == 0) {
/* Reply to CAPABILITY command we sent */
client->proxy_state = IMAP_PROXY_STATE_CAPABILITY;
if (strncmp(line, "C OK ", 5) == 0 &&
client->proxy_password != NULL) {
/* pipelining was disabled, send the login now. */
str = t_str_new(128);
if (proxy_write_login(imap_client, str) < 0)
return -1;
o_stream_nsend(output, str_data(str), str_len(str));
return 1;
}
return 0;
} else if (strncasecmp(line, "I ", 2) == 0) {
/* Reply to ID command we sent, ignore it unless
pipelining is disabled, in which case send
either STARTTLS or login */
client->proxy_state = IMAP_PROXY_STATE_ID;
if (client->proxy_nopipelining) {
str = t_str_new(128);
if ((ret = proxy_write_starttls(imap_client, str)) < 0) {
return -1;
} else if (ret == 0) {
if (proxy_write_login(imap_client, str) < 0)
return -1;
}
o_stream_nsend(output, str_data(str), str_len(str));
return 1;
}
return 0;
} else if (strncasecmp(line, "* ID ", 5) == 0) {
/* Reply to ID command we sent, ignore it */
return 0;
} else if (strncmp(line, "* ", 2) == 0) {
/* untagged reply. just forward it. */
client_send_raw(client, t_strconcat(line, "\r\n", NULL));
return 0;
} else {
/* tagged reply, shouldn't happen. */
client_log_err(client, t_strdup_printf(
"proxy: Unexpected input, ignoring: %s",
str_sanitize(line, 160)));
return 0;
}
}
void imap_proxy_reset(struct client *client)
{
struct imap_client *imap_client = (struct imap_client *)client;
imap_client->proxy_sasl_ir = FALSE;
imap_client->proxy_logindisabled = FALSE;
imap_client->proxy_seen_banner = FALSE;
imap_client->proxy_capability_request_sent = FALSE;
client->proxy_state = IMAP_PROXY_STATE_NONE;
}
void imap_proxy_error(struct client *client, const char *text)
{
client_send_reply_code(client, IMAP_CMD_REPLY_NO,
IMAP_RESP_CODE_UNAVAILABLE, text);
}