imap-proxy.c revision 97db4761382024093f441e4bc78ba8b6a056504d
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen/* Copyright (c) 2004-2009 Dovecot authors, see the included COPYING file */
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#include "common.h"
345648b341f228bd7f0b89f8aa3ecb9c470d817eTimo Sirainen#include "array.h"
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#include "ioloop.h"
d1414c09cf0d58ac983054e2f4e1a1f329272dcfTimo Sirainen#include "istream.h"
5a2cb3d097a2d9a9e930af997e7bf3400a8d840dTimo Sirainen#include "ostream.h"
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#include "base64.h"
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#include "str.h"
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#include "str-sanitize.h"
97c339398f1aba6f315b55a9b6ee6b020e33bea4Timo Sirainen#include "safe-memset.h"
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#include "client.h"
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#include "client-authenticate.h"
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#include "imap-resp-code.h"
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#include "imap-quote.h"
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#include "imap-proxy.h"
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#include <stdlib.h>
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen#define PROXY_FAILURE_MSG \
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen "NO ["IMAP_RESP_CODE_UNAVAILABLE"] "AUTH_TEMP_FAILED_MSG
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainenstatic const char *const *
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainencapabilities_strip_prelogin(const char *const *capabilities)
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen{
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen ARRAY_TYPE(const_string) new_caps_arr;
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen const char **new_caps, *str;
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen unsigned int count;
d1414c09cf0d58ac983054e2f4e1a1f329272dcfTimo Sirainen
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen t_array_init(&new_caps_arr, 64);
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen for (; *capabilities != NULL; capabilities++) {
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen if (strncasecmp(*capabilities, "AUTH=", 5) == 0 ||
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen strcasecmp(*capabilities, "STARTTLS") == 0 ||
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen strcasecmp(*capabilities, "SASL-IR") == 0 ||
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen strcasecmp(*capabilities, "LOGINDISABLED") == 0 ||
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen strcasecmp(*capabilities, "LOGIN-REFERRALS") == 0)
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen continue;
73bfdbe28c2ce6d143eadf0bab8ccfbe4cab0faeTimo Sirainen
d1414c09cf0d58ac983054e2f4e1a1f329272dcfTimo Sirainen str = *capabilities;
d1414c09cf0d58ac983054e2f4e1a1f329272dcfTimo Sirainen array_append(&new_caps_arr, &str, 1);
d1414c09cf0d58ac983054e2f4e1a1f329272dcfTimo Sirainen }
d1414c09cf0d58ac983054e2f4e1a1f329272dcfTimo Sirainen new_caps = array_get_modifiable(&new_caps_arr, &count);
c57776c06ec99ba9b0dafdbf9475ea72ea8ca134Timo Sirainen qsort(new_caps, count, sizeof(*new_caps), i_strcasecmp_p);
d1414c09cf0d58ac983054e2f4e1a1f329272dcfTimo Sirainen
d1414c09cf0d58ac983054e2f4e1a1f329272dcfTimo Sirainen (void)array_append_space(&new_caps_arr);
d1414c09cf0d58ac983054e2f4e1a1f329272dcfTimo Sirainen return array_idx(&new_caps_arr, 0);
d1414c09cf0d58ac983054e2f4e1a1f329272dcfTimo Sirainen}
d1414c09cf0d58ac983054e2f4e1a1f329272dcfTimo Sirainen
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainenstatic void proxy_write_id(struct imap_client *client, string_t *str)
33c6d5807b449463e9b81db5ec99fe027cc1b984Timo Sirainen{
8eea67470c1bd8562a62e7445d930bb2079b1a43Timo Sirainen str_printfa(str, "I ID ("
94a78eb438622fa53abef1e1726714dacad4b61cTimo Sirainen "\"x-originating-ip\" \"%s\" "
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen "\"x-originating-port\" \"%u\" "
c7480644202e5451fbed448508ea29a25cffc99cTimo Sirainen "\"x-connected-ip\" \"%s\" "
a4ac325c2802693c6b761e5a8fda961e5d7490eaTimo Sirainen "\"x-connected-port\" \"%u\")\r\n",
b7c2065b3f10f9ae27787a9db5aaefbfc70d4502Timo Sirainen net_ip2addr(&client->common.ip),
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen client->common.remote_port,
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen net_ip2addr(&client->common.local_ip),
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen client->common.local_port);
97c339398f1aba6f315b55a9b6ee6b020e33bea4Timo Sirainen}
97c339398f1aba6f315b55a9b6ee6b020e33bea4Timo Sirainen
97c339398f1aba6f315b55a9b6ee6b020e33bea4Timo Sirainenstatic void proxy_free_password(struct imap_client *client)
97c339398f1aba6f315b55a9b6ee6b020e33bea4Timo Sirainen{
97c339398f1aba6f315b55a9b6ee6b020e33bea4Timo Sirainen if (client->proxy_password == NULL)
97c339398f1aba6f315b55a9b6ee6b020e33bea4Timo Sirainen return;
97c339398f1aba6f315b55a9b6ee6b020e33bea4Timo Sirainen
97c339398f1aba6f315b55a9b6ee6b020e33bea4Timo Sirainen safe_memset(client->proxy_password, 0, strlen(client->proxy_password));
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen i_free_and_null(client->proxy_password);
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen}
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainenstatic void proxy_failed(struct imap_client *client, bool send_tagline)
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen{
33c6d5807b449463e9b81db5ec99fe027cc1b984Timo Sirainen if (send_tagline)
8eea67470c1bd8562a62e7445d930bb2079b1a43Timo Sirainen client_send_tagline(client, PROXY_FAILURE_MSG);
94a78eb438622fa53abef1e1726714dacad4b61cTimo Sirainen
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen login_proxy_free(&client->proxy);
c7480644202e5451fbed448508ea29a25cffc99cTimo Sirainen proxy_free_password(client);
a4ac325c2802693c6b761e5a8fda961e5d7490eaTimo Sirainen i_free_and_null(client->proxy_user);
b7c2065b3f10f9ae27787a9db5aaefbfc70d4502Timo Sirainen i_free_and_null(client->proxy_master_user);
25757faf029c369a8318349dafe952e2358df1d8Timo Sirainen
/* call this last - it may destroy the client */
client_auth_failed(client, TRUE);
}
static void get_plain_auth(struct imap_client *client, string_t *dest)
{
string_t *str;
str = t_str_new(128);
str_append(str, client->proxy_user);
str_append_c(str, '\0');
str_append(str, client->proxy_master_user);
str_append_c(str, '\0');
str_append(str, client->proxy_password);
base64_encode(str_data(str), str_len(str), dest);
}
static bool str_array_icmp(const char *const *arr1, const char *const *arr2)
{
unsigned int i;
for (i = 0; arr1[i] != NULL; i++) {
if (arr2[i] == NULL || strcasecmp(arr1[i], arr2[i]) != 0)
return FALSE;
}
return TRUE;
}
static void
client_send_capability_if_needed(struct imap_client *client, string_t *str,
const char *capability)
{
const char *const *backend_capabilities;
const char *const *proxy_capabilities;
if (!client->capability_command_used || capability == NULL)
return;
/* reset this so that we don't re-send the CAPABILITY in case server
sends it multiple times */
client->capability_command_used = FALSE;
/* client has used CAPABILITY command, so it didn't understand the
capabilities in the banner. if backend server has different
capabilities than we advertised already, there's a problem.
to solve that we'll send the backend's untagged CAPABILITY reply
and hope that the client understands it */
backend_capabilities =
capabilities_strip_prelogin(t_strsplit(capability, " "));
proxy_capabilities =
capabilities_strip_prelogin(t_strsplit(client->common.set->capability_string, " "));
if (str_array_icmp(backend_capabilities, proxy_capabilities))
return;
str_printfa(str, "* CAPABILITY %s\r\n", capability);
}
static void proxy_write_login(struct imap_client *client, string_t *str)
{
if (client->capability_command_used)
str_append(str, "C CAPABILITY\r\n");
if (client->proxy_master_user == NULL) {
/* logging in normally - use LOGIN command */
str_append(str, "L LOGIN ");
imap_quote_append_string(str, client->proxy_user, FALSE);
str_append_c(str, ' ');
imap_quote_append_string(str, client->proxy_password, FALSE);
proxy_free_password(client);
} else if (client->proxy_sasl_ir) {
/* master user login with SASL initial response support */
str_append(str, "L AUTHENTICATE PLAIN ");
get_plain_auth(client, str);
proxy_free_password(client);
} else {
/* master user login without SASL initial response */
str_append(str, "L AUTHENTICATE PLAIN");
}
str_append(str, "\r\n");
}
static int proxy_input_banner(struct imap_client *client,
struct ostream *output, const char *line)
{
enum login_proxy_ssl_flags ssl_flags;
const char *const *capabilities = NULL;
string_t *str;
if (strncmp(line, "* OK ", 5) != 0) {
client_syslog_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, "ID"))
proxy_write_id(client, str);
if (str_array_icase_find(capabilities, "SASL-IR"))
client->proxy_sasl_ir = TRUE;
}
ssl_flags = login_proxy_get_ssl_flags(client->proxy);
if ((ssl_flags & PROXY_SSL_FLAG_STARTTLS) != 0) {
if (capabilities != NULL &&
!str_array_icase_find(capabilities, "STARTTLS")) {
client_syslog_err(&client->common,
"proxy: Remote doesn't support STARTTLS");
return -1;
}
str_append(str, "S STARTTLS\r\n");
} else {
proxy_write_login(client, str);
}
(void)o_stream_send(output, str_data(str), str_len(str));
return 0;
}
static int proxy_input_line(struct imap_client *client, const char *line)
{
struct ostream *output;
const char *capability;
string_t *str;
i_assert(!client->destroyed);
output = login_proxy_get_ostream(client->proxy);
if (!client->proxy_seen_banner) {
/* this is a banner */
client->proxy_seen_banner = TRUE;
if (proxy_input_banner(client, output, line) < 0) {
proxy_failed(client, TRUE);
return -1;
}
return 0;
} else if (*line == '+') {
/* AUTHENTICATE started. finish it. */
str = t_str_new(128);
get_plain_auth(client, str);
str_append(str, "\r\n");
proxy_free_password(client);
(void)o_stream_send(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_syslog_err(&client->common, t_strdup_printf(
"proxy: Remote STARTTLS failed: %s",
str_sanitize(line + 5, 160)));
proxy_failed(client, TRUE);
return -1;
}
/* STARTTLS successful, begin TLS negotiation. */
if (login_proxy_starttls(client->proxy) < 0) {
proxy_failed(client, TRUE);
return -1;
}
/* i/ostreams changed. */
output = login_proxy_get_ostream(client->proxy);
str = t_str_new(128);
proxy_write_login(client, str);
(void)o_stream_send(output, str_data(str), str_len(str));
return 1;
} else if (strncmp(line, "L OK ", 5) == 0) {
/* Login successful. Send this line to client. */
capability = client->proxy_backend_capability;
if (strncmp(line + 5, "[CAPABILITY ", 12) == 0)
capability = t_strcut(line + 5 + 12, ']');
str = t_str_new(128);
client_send_capability_if_needed(client, str, capability);
str_append(str, client->cmd_tag);
str_append(str, line + 1);
str_append(str, "\r\n");
(void)o_stream_send(client->output,
str_data(str), str_len(str));
str_truncate(str, 0);
str_printfa(str, "proxy(%s): started proxying to %s:%u",
client->common.virtual_user,
login_proxy_get_host(client->proxy),
login_proxy_get_port(client->proxy));
if (strcmp(client->common.virtual_user,
client->proxy_user) != 0) {
/* remote username is different, log it */
str_append_c(str, '/');
str_append(str, client->proxy_user);
}
if (client->proxy_master_user != NULL) {
str_printfa(str, " (master %s)",
client->proxy_master_user);
}
(void)client_skip_line(client);
login_proxy_detach(client->proxy, client->common.input,
client->output);
client->proxy = NULL;
client->common.input = NULL;
client->output = NULL;
client->common.fd = -1;
client->common.proxying = TRUE;
client_destroy_success(client, str_c(str));
return 1;
} else if (strncmp(line, "L ", 2) == 0) {
line += 2;
if (client->common.set->verbose_auth) {
str = t_str_new(128);
str_printfa(str, "proxy(%s): Login failed to %s:%u",
client->common.virtual_user,
login_proxy_get_host(client->proxy),
login_proxy_get_port(client->proxy));
if (strcmp(client->common.virtual_user,
client->proxy_user) != 0) {
/* remote username is different, log it */
str_append_c(str, '/');
str_append(str, client->proxy_user);
}
if (client->proxy_master_user != NULL) {
str_printfa(str, " (master %s)",
client->proxy_master_user);
}
str_append(str, ": ");
if (strncasecmp(line, "NO ", 3) == 0)
str_append(str, line + 3);
else
str_append(str, line);
i_info("%s", str_c(str));
}
#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. */
line = "NO "IMAP_AUTH_FAILED_MSG;
} else if (strncmp(line, "NO [", 4) == 0) {
/* remote sent some other resp-code. forward it. */
} 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. */
line = "NO "IMAP_AUTH_FAILED_MSG;
}
client_send_tagline(client, line);
proxy_failed(client, FALSE);
return -1;
} else if (strncasecmp(line, "* CAPABILITY ", 13) == 0) {
i_free(client->proxy_backend_capability);
client->proxy_backend_capability = i_strdup(line + 13);
return 0;
} else {
/* probably some untagged reply */
return 0;
}
}
static void proxy_input(struct imap_client *client)
{
struct istream *input;
const char *line;
if (client->proxy == NULL) {
/* we're just freeing the proxy */
return;
}
input = login_proxy_get_istream(client->proxy);
if (input == NULL) {
if (client->destroyed) {
/* we came here from client_destroy() */
return;
}
/* failed for some reason, probably server disconnected */
proxy_failed(client, TRUE);
return;
}
i_assert(!client->destroyed);
switch (i_stream_read(input)) {
case -2:
client_syslog_err(&client->common,
"proxy: Remote input buffer full");
proxy_failed(client, TRUE);
return;
case -1:
client_syslog_err(&client->common,
"proxy: Remote disconnected");
proxy_failed(client, TRUE);
return;
}
while ((line = i_stream_next_line(input)) != NULL) {
if (proxy_input_line(client, line) != 0)
break;
}
}
int imap_proxy_new(struct imap_client *client, const char *host,
unsigned int port, const char *user, const char *master_user,
const char *password, enum login_proxy_ssl_flags ssl_flags)
{
i_assert(user != NULL);
i_assert(!client->destroyed);
if (password == NULL) {
client_syslog_err(&client->common, "proxy: password not given");
client_send_tagline(client, PROXY_FAILURE_MSG);
return -1;
}
i_assert(client->refcount > 1);
if (client->destroyed) {
/* connection_queue_add() decided that we were the oldest
connection and killed us. */
return -1;
}
if (login_proxy_is_ourself(&client->common, host, port, user)) {
client_syslog_err(&client->common, "Proxying loops to itself");
client_send_tagline(client, PROXY_FAILURE_MSG);
return -1;
}
client->proxy = login_proxy_new(&client->common, host, port, ssl_flags,
proxy_input, client);
if (client->proxy == NULL) {
client_send_tagline(client, PROXY_FAILURE_MSG);
return -1;
}
client->proxy_sasl_ir = FALSE;
client->proxy_seen_banner = FALSE;
client->proxy_user = i_strdup(user);
client->proxy_master_user = i_strdup(master_user);
client->proxy_password = i_strdup(password);
/* disable input until authentication is finished */
if (client->io != NULL)
io_remove(&client->io);
return 0;
}