Mercurial > dovecot > core-2.2
view src/login-common/login-proxy.c @ 14133:ba770cba5598
Updated copyright notices to include year 2012.
author | Timo Sirainen <tss@iki.fi> |
---|---|
date | Sun, 12 Feb 2012 18:55:28 +0200 |
parents | 7e3afd2252fd |
children | da36d22ab37a c3da6b3a4a34 |
line wrap: on
line source
/* Copyright (c) 2004-2012 Dovecot authors, see the included COPYING file */ #include "login-common.h" #include "ioloop.h" #include "istream.h" #include "ostream.h" #include "llist.h" #include "md5.h" #include "str-sanitize.h" #include "time-util.h" #include "master-service.h" #include "ipc-server.h" #include "dns-lookup.h" #include "client-common.h" #include "ssl-proxy.h" #include "login-proxy-state.h" #include "login-proxy.h" #define MAX_PROXY_INPUT_SIZE 4096 #define OUTBUF_THRESHOLD 1024 #define LOGIN_PROXY_DIE_IDLE_SECS 2 #define LOGIN_PROXY_DNS_WARN_MSECS 500 #define LOGIN_PROXY_IPC_PATH "ipc-proxy" #define LOGIN_PROXY_IPC_NAME "proxy" #define KILLED_BY_ADMIN_REASON "Killed by admin" struct login_proxy { struct login_proxy *prev, *next; struct client *client; int client_fd, server_fd; struct io *client_io, *server_io; struct istream *server_input; struct ostream *client_output, *server_output; struct ssl_proxy *ssl_server_proxy; time_t last_io; struct timeval created; struct timeout *to, *to_notify; struct login_proxy_record *state_rec; struct ip_addr ip; char *host; unsigned int port; unsigned int connect_timeout_msecs; unsigned int notify_refresh_secs; enum login_proxy_ssl_flags ssl_flags; proxy_callback_t *callback; unsigned int destroying:1; unsigned int disconnecting:1; }; static struct login_proxy_state *proxy_state; static struct login_proxy *login_proxies = NULL; static struct login_proxy *login_proxies_pending = NULL; static struct ipc_server *login_proxy_ipc_server; static void login_proxy_ipc_cmd(struct ipc_cmd *cmd, const char *line); static void login_proxy_free_reason(struct login_proxy **_proxy, const char *reason); static void login_proxy_free_errno(struct login_proxy **proxy, int err, const char *who) { const char *reason; reason = err == 0 || err == EPIPE ? t_strdup_printf("Disconnected by %s", who) : t_strdup_printf("Disconnected by %s: %s", who, strerror(errno)); login_proxy_free_reason(proxy, reason); } static void server_input(struct login_proxy *proxy) { unsigned char buf[OUTBUF_THRESHOLD]; ssize_t ret; proxy->last_io = ioloop_time; if (o_stream_get_buffer_used_size(proxy->client_output) > OUTBUF_THRESHOLD) { /* client's output buffer is already quite full. don't send more until we're below threshold. */ io_remove(&proxy->server_io); return; } ret = net_receive(proxy->server_fd, buf, sizeof(buf)); if (ret < 0) login_proxy_free_errno(&proxy, errno, "server"); else if (o_stream_send(proxy->client_output, buf, ret) != ret) { login_proxy_free_errno(&proxy, proxy->client_output->stream_errno, "client"); } } static void proxy_client_input(struct login_proxy *proxy) { unsigned char buf[OUTBUF_THRESHOLD]; ssize_t ret; proxy->last_io = ioloop_time; if (o_stream_get_buffer_used_size(proxy->server_output) > OUTBUF_THRESHOLD) { /* proxy's output buffer is already quite full. don't send more until we're below threshold. */ io_remove(&proxy->client_io); return; } ret = net_receive(proxy->client_fd, buf, sizeof(buf)); if (ret < 0) login_proxy_free_errno(&proxy, errno, "client"); else if (o_stream_send(proxy->server_output, buf, ret) != ret) { login_proxy_free_errno(&proxy, proxy->server_output->stream_errno, "server"); } } static int server_output(struct login_proxy *proxy) { proxy->last_io = ioloop_time; if (o_stream_flush(proxy->server_output) < 0) { login_proxy_free_errno(&proxy, proxy->server_output->stream_errno, "server"); return 1; } if (proxy->client_io == NULL && o_stream_get_buffer_used_size(proxy->server_output) < OUTBUF_THRESHOLD) { /* there's again space in proxy's output buffer, so we can read more from client. */ proxy->client_io = io_add(proxy->client_fd, IO_READ, proxy_client_input, proxy); } return 1; } static int proxy_client_output(struct login_proxy *proxy) { proxy->last_io = ioloop_time; if (o_stream_flush(proxy->client_output) < 0) { login_proxy_free_errno(&proxy, proxy->client_output->stream_errno, "client"); return 1; } if (proxy->server_io == NULL && o_stream_get_buffer_used_size(proxy->client_output) < OUTBUF_THRESHOLD) { /* there's again space in client's output buffer, so we can read more from proxy. */ proxy->server_io = io_add(proxy->server_fd, IO_READ, server_input, proxy); } return 1; } static void proxy_prelogin_input(struct login_proxy *proxy) { proxy->callback(proxy->client); } static void proxy_plain_connected(struct login_proxy *proxy) { proxy->server_input = i_stream_create_fd(proxy->server_fd, MAX_PROXY_INPUT_SIZE, FALSE); proxy->server_output = o_stream_create_fd(proxy->server_fd, (size_t)-1, FALSE); proxy->server_io = io_add(proxy->server_fd, IO_READ, proxy_prelogin_input, proxy); } static void proxy_fail_connect(struct login_proxy *proxy) { if (timeval_cmp(&proxy->created, &proxy->state_rec->last_success) < 0) { /* there was a successful connection done since we started connecting. perhaps this is just a temporary one-off failure. */ } else { proxy->state_rec->last_failure = ioloop_timeval; } proxy->state_rec->num_waiting_connections--; proxy->state_rec = NULL; } static void proxy_wait_connect(struct login_proxy *proxy) { int err; err = net_geterror(proxy->server_fd); if (err != 0) { i_error("proxy(%s): connect(%s, %u) failed: %s", proxy->client->virtual_user, proxy->host, proxy->port, strerror(err)); proxy_fail_connect(proxy); login_proxy_free(&proxy); return; } proxy->state_rec->last_success = ioloop_timeval; proxy->state_rec->num_waiting_connections--; proxy->state_rec = NULL; if (proxy->to != NULL) timeout_remove(&proxy->to); if ((proxy->ssl_flags & PROXY_SSL_FLAG_YES) != 0 && (proxy->ssl_flags & PROXY_SSL_FLAG_STARTTLS) == 0) { if (login_proxy_starttls(proxy) < 0) { login_proxy_free(&proxy); return; } } else { io_remove(&proxy->server_io); proxy_plain_connected(proxy); } } static void proxy_connect_timeout(struct login_proxy *proxy) { i_error("proxy(%s): connect(%s, %u) timed out", proxy->client->virtual_user, proxy->host, proxy->port); proxy_fail_connect(proxy); login_proxy_free(&proxy); } static int login_proxy_connect(struct login_proxy *proxy) { struct login_proxy_record *rec; rec = login_proxy_state_get(proxy_state, &proxy->ip, proxy->port); if (timeval_cmp(&rec->last_failure, &rec->last_success) > 0 && rec->num_waiting_connections != 0) { /* the server is down. fail immediately */ i_error("proxy(%s): Host %s:%u is down", proxy->client->virtual_user, proxy->host, proxy->port); login_proxy_free(&proxy); return -1; } proxy->server_fd = net_connect_ip(&proxy->ip, proxy->port, NULL); if (proxy->server_fd == -1) { i_error("proxy(%s): connect(%s, %u) failed: %m", proxy->client->virtual_user, proxy->host, proxy->port); login_proxy_free(&proxy); return -1; } proxy->server_io = io_add(proxy->server_fd, IO_WRITE, proxy_wait_connect, proxy); if (proxy->connect_timeout_msecs != 0) { proxy->to = timeout_add(proxy->connect_timeout_msecs, proxy_connect_timeout, proxy); } proxy->state_rec = rec; proxy->state_rec->num_waiting_connections++; return 0; } static void login_proxy_dns_done(const struct dns_lookup_result *result, struct login_proxy *proxy) { if (result->ret != 0) { i_error("proxy(%s): DNS lookup of %s failed: %s", proxy->client->virtual_user, proxy->host, result->error); login_proxy_free(&proxy); } else { if (result->msecs > LOGIN_PROXY_DNS_WARN_MSECS) { i_warning("proxy(%s): DNS lookup for %s took %u.%03u s", proxy->client->virtual_user, proxy->host, result->msecs/1000, result->msecs % 1000); } proxy->ip = result->ips[0]; (void)login_proxy_connect(proxy); } } int login_proxy_new(struct client *client, const struct login_proxy_settings *set, proxy_callback_t *callback) { struct login_proxy *proxy; struct dns_lookup_settings dns_lookup_set; i_assert(client->login_proxy == NULL); if (set->host == NULL || *set->host == '\0') { i_error("proxy(%s): host not given", client->virtual_user); return -1; } proxy = i_new(struct login_proxy, 1); proxy->client = client; proxy->client_fd = -1; proxy->server_fd = -1; proxy->created = ioloop_timeval; proxy->host = i_strdup(set->host); proxy->port = set->port; proxy->connect_timeout_msecs = set->connect_timeout_msecs; proxy->notify_refresh_secs = set->notify_refresh_secs; proxy->ssl_flags = set->ssl_flags; client_ref(client); memset(&dns_lookup_set, 0, sizeof(dns_lookup_set)); dns_lookup_set.dns_client_socket_path = set->dns_client_socket_path; dns_lookup_set.timeout_msecs = set->connect_timeout_msecs; if (net_addr2ip(set->host, &proxy->ip) < 0) { if (dns_lookup(set->host, &dns_lookup_set, login_proxy_dns_done, proxy) < 0) return -1; } else { if (login_proxy_connect(proxy) < 0) return -1; } DLLIST_PREPEND(&login_proxies_pending, proxy); proxy->callback = callback; client->login_proxy = proxy; return 0; } static void login_proxy_free_reason(struct login_proxy **_proxy, const char *reason) { struct login_proxy *proxy = *_proxy; struct client *client = proxy->client; const char *ipstr; *_proxy = NULL; if (proxy->destroying) return; proxy->destroying = TRUE; if (proxy->to != NULL) timeout_remove(&proxy->to); if (proxy->to_notify != NULL) timeout_remove(&proxy->to_notify); if (proxy->state_rec != NULL) proxy->state_rec->num_waiting_connections--; if (proxy->to != NULL) timeout_remove(&proxy->to); if (proxy->server_io != NULL) io_remove(&proxy->server_io); if (proxy->server_input != NULL) i_stream_destroy(&proxy->server_input); if (proxy->server_output != NULL) o_stream_destroy(&proxy->server_output); if (proxy->client_fd != -1) { /* detached proxy */ DLLIST_REMOVE(&login_proxies, proxy); ipstr = net_ip2addr(&proxy->client->ip); i_info("proxy(%s): disconnecting %s%s", proxy->client->virtual_user, ipstr != NULL ? ipstr : "", reason == NULL ? "" : t_strdup_printf(" (%s)", reason)); if (proxy->client_io != NULL) io_remove(&proxy->client_io); if (proxy->client_output != NULL) o_stream_destroy(&proxy->client_output); net_disconnect(proxy->client_fd); } else { i_assert(proxy->client_io == NULL); i_assert(proxy->client_output == NULL); DLLIST_REMOVE(&login_proxies_pending, proxy); if (proxy->callback != NULL) proxy->callback(proxy->client); } if (proxy->server_fd != -1) net_disconnect(proxy->server_fd); if (proxy->ssl_server_proxy != NULL) ssl_proxy_free(&proxy->ssl_server_proxy); i_free(proxy->host); i_free(proxy); client->login_proxy = NULL; client_unref(&client); } void login_proxy_free(struct login_proxy **_proxy) { login_proxy_free_reason(_proxy, NULL); } bool login_proxy_is_ourself(const struct client *client, const char *host, unsigned int port, const char *destuser) { struct ip_addr ip; if (port != client->local_port) return FALSE; if (net_addr2ip(host, &ip) < 0) return FALSE; if (!net_ip_compare(&ip, &client->local_ip)) return FALSE; return strcmp(client->virtual_user, destuser) == 0; } struct istream *login_proxy_get_istream(struct login_proxy *proxy) { return proxy->disconnecting ? NULL : proxy->server_input; } struct ostream *login_proxy_get_ostream(struct login_proxy *proxy) { return proxy->server_output; } const char *login_proxy_get_host(const struct login_proxy *proxy) { return proxy->host; } unsigned int login_proxy_get_port(const struct login_proxy *proxy) { return proxy->port; } enum login_proxy_ssl_flags login_proxy_get_ssl_flags(const struct login_proxy *proxy) { return proxy->ssl_flags; } static void login_proxy_notify(struct login_proxy *proxy) { login_proxy_state_notify(proxy_state, proxy->client->proxy_user); } void login_proxy_detach(struct login_proxy *proxy) { struct client *client = proxy->client; const unsigned char *data; size_t size; i_assert(proxy->client_fd == -1); i_assert(proxy->server_output != NULL); proxy->client_fd = i_stream_get_fd(client->input); proxy->client_output = client->output; o_stream_set_max_buffer_size(client->output, (size_t)-1); o_stream_set_flush_callback(client->output, proxy_client_output, proxy); client->output = NULL; /* send all pending client input to proxy and get rid of the stream */ data = i_stream_get_data(client->input, &size); if (size != 0) (void)o_stream_send(proxy->server_output, data, size); /* from now on, just do dummy proxying */ io_remove(&proxy->server_io); proxy->server_io = io_add(proxy->server_fd, IO_READ, server_input, proxy); proxy->client_io = io_add(proxy->client_fd, IO_READ, proxy_client_input, proxy); o_stream_set_flush_callback(proxy->server_output, server_output, proxy); i_stream_destroy(&proxy->server_input); if (proxy->notify_refresh_secs != 0) { proxy->to_notify = timeout_add(proxy->notify_refresh_secs * 1000, login_proxy_notify, proxy); } proxy->callback = NULL; if (login_proxy_ipc_server == NULL) { login_proxy_ipc_server = ipc_server_init(LOGIN_PROXY_IPC_PATH, LOGIN_PROXY_IPC_NAME, login_proxy_ipc_cmd); } DLLIST_REMOVE(&login_proxies_pending, proxy); DLLIST_PREPEND(&login_proxies, proxy); client->fd = -1; client->login_proxy = NULL; } static int login_proxy_ssl_handshaked(void *context) { struct login_proxy *proxy = context; if ((proxy->ssl_flags & PROXY_SSL_FLAG_ANY_CERT) != 0) return 0; if (ssl_proxy_has_broken_client_cert(proxy->ssl_server_proxy)) { client_log_err(proxy->client, t_strdup_printf( "proxy: Received invalid SSL certificate from %s:%u", proxy->host, proxy->port)); } else if (!ssl_proxy_has_valid_client_cert(proxy->ssl_server_proxy)) { client_log_err(proxy->client, t_strdup_printf( "proxy: SSL certificate not received from %s:%u", proxy->host, proxy->port)); } else if (ssl_proxy_cert_match_name(proxy->ssl_server_proxy, proxy->host) < 0) { client_log_err(proxy->client, t_strdup_printf( "proxy: hostname doesn't match SSL certificate at %s:%u", proxy->host, proxy->port)); } else { return 0; } proxy->disconnecting = TRUE; return -1; } int login_proxy_starttls(struct login_proxy *proxy) { int fd; if (proxy->server_input != NULL) i_stream_destroy(&proxy->server_input); if (proxy->server_output != NULL) o_stream_destroy(&proxy->server_output); io_remove(&proxy->server_io); fd = ssl_proxy_client_alloc(proxy->server_fd, &proxy->client->ip, proxy->client->set, login_proxy_ssl_handshaked, proxy, &proxy->ssl_server_proxy); if (fd < 0) { client_log_err(proxy->client, t_strdup_printf( "proxy: SSL handshake failed to %s:%u", proxy->host, proxy->port)); return -1; } ssl_proxy_set_client(proxy->ssl_server_proxy, proxy->client); ssl_proxy_start(proxy->ssl_server_proxy); proxy->server_fd = fd; proxy_plain_connected(proxy); return 0; } static void proxy_kill_idle(struct login_proxy *proxy) { login_proxy_free_reason(&proxy, KILLED_BY_ADMIN_REASON); } void login_proxy_kill_idle(void) { struct login_proxy *proxy, *next; time_t now = time(NULL); time_t stop_timestamp = now - LOGIN_PROXY_DIE_IDLE_SECS; unsigned int stop_msecs; for (proxy = login_proxies; proxy != NULL; proxy = next) { next = proxy->next; if (proxy->last_io <= stop_timestamp) proxy_kill_idle(proxy); else { i_assert(proxy->to == NULL); stop_msecs = (proxy->last_io - stop_timestamp) * 1000; proxy->to = timeout_add(stop_msecs, proxy_kill_idle, proxy); } } } static void login_proxy_cmd_kick(struct ipc_cmd *cmd, const char *const *args) { struct login_proxy *proxy, *next; unsigned int count = 0; if (args[0] == NULL) { ipc_cmd_fail(&cmd, "Missing parameter"); return; } for (proxy = login_proxies; proxy != NULL; proxy = next) { next = proxy->next; if (strcmp(proxy->client->virtual_user, args[0]) == 0) { login_proxy_free_reason(&proxy, KILLED_BY_ADMIN_REASON); count++; } } for (proxy = login_proxies_pending; proxy != NULL; proxy = next) { next = proxy->next; if (strcmp(proxy->client->virtual_user, args[0]) == 0) { client_destroy(proxy->client, "Connection kicked"); count++; } } ipc_cmd_success_reply(&cmd, t_strdup_printf("%u", count)); } static unsigned int director_username_hash(const char *username) { /* NOTE: If you modify this, modify also user_directory_get_username_hash() in director/user-director.c */ unsigned char md5[MD5_RESULTLEN]; unsigned int i, hash = 0; md5_get_digest(username, strlen(username), md5); for (i = 0; i < sizeof(hash); i++) hash = (hash << CHAR_BIT) | md5[i]; return hash; } static void login_proxy_cmd_kick_director_hash(struct ipc_cmd *cmd, const char *const *args) { struct login_proxy *proxy, *next; unsigned int hash, count = 0; if (args[0] == NULL || str_to_uint(args[0], &hash) < 0) { ipc_cmd_fail(&cmd, "Invalid parameters"); return; } for (proxy = login_proxies; proxy != NULL; proxy = next) { next = proxy->next; if (director_username_hash(proxy->client->virtual_user) == hash) { login_proxy_free_reason(&proxy, KILLED_BY_ADMIN_REASON); count++; } } for (proxy = login_proxies_pending; proxy != NULL; proxy = next) { next = proxy->next; if (director_username_hash(proxy->client->virtual_user) == hash) { client_destroy(proxy->client, "Connection kicked"); count++; } } ipc_cmd_success_reply(&cmd, t_strdup_printf("%u", count)); } static void login_proxy_cmd_list_reply(struct ipc_cmd *cmd, struct login_proxy *proxy) { T_BEGIN { const char *reply; reply = t_strdup_printf("%s\t%s\t%s\t%s\t%u", proxy->client->virtual_user, login_binary->protocol, net_ip2addr(&proxy->client->ip), net_ip2addr(&proxy->ip), proxy->port); ipc_cmd_send(cmd, reply); } T_END; } static void login_proxy_cmd_list(struct ipc_cmd *cmd, const char *const *args ATTR_UNUSED) { struct login_proxy *proxy; for (proxy = login_proxies; proxy != NULL; proxy = proxy->next) login_proxy_cmd_list_reply(cmd, proxy); for (proxy = login_proxies_pending; proxy != NULL; proxy = proxy->next) login_proxy_cmd_list_reply(cmd, proxy); ipc_cmd_success(&cmd); } static void login_proxy_ipc_cmd(struct ipc_cmd *cmd, const char *line) { const char *const *args = t_strsplit(line, "\t"); const char *name = args[0]; args++; if (strcmp(name, "KICK") == 0) login_proxy_cmd_kick(cmd, args); else if (strcmp(name, "KICK-DIRECTOR-HASH") == 0) login_proxy_cmd_kick_director_hash(cmd, args); else if (strcmp(name, "LIST") == 0) login_proxy_cmd_list(cmd, args); else ipc_cmd_fail(&cmd, "Unknown command"); } void login_proxy_init(const char *proxy_notify_pipe_path) { proxy_state = login_proxy_state_init(proxy_notify_pipe_path); } void login_proxy_deinit(void) { struct login_proxy *proxy; while (login_proxies != NULL) { proxy = login_proxies; login_proxy_free_reason(&proxy, KILLED_BY_ADMIN_REASON); } if (login_proxy_ipc_server != NULL) ipc_server_deinit(&login_proxy_ipc_server); login_proxy_state_deinit(&proxy_state); }