diff options
Diffstat (limited to 'src/client.c')
-rw-r--r-- | src/client.c | 787 |
1 files changed, 787 insertions, 0 deletions
diff --git a/src/client.c b/src/client.c new file mode 100644 index 000000000..6a43b9586 --- /dev/null +++ b/src/client.c @@ -0,0 +1,787 @@ +/* the Music Player Daemon (MPD) + * Copyright (C) 2003-2007 by Warren Dukes (warren.dukes@gmail.com) + * This project's homepage is: http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "client.h" +#include "command.h" +#include "conf.h" +#include "log.h" +#include "listen.h" +#include "permission.h" +#include "sllist.h" +#include "utils.h" +#include "ioops.h" +#include "myfprintf.h" +#include "os_compat.h" +#include "main_notify.h" +#include "dlist.h" + +#include "../config.h" + +#define GREETING "OK MPD " PROTOCOL_VERSION "\n" + +#define CLIENT_MAX_BUFFER_LENGTH (40960) +#define CLIENT_LIST_MODE_BEGIN "command_list_begin" +#define CLIENT_LIST_OK_MODE_BEGIN "command_list_ok_begin" +#define CLIENT_LIST_MODE_END "command_list_end" +#define CLIENT_DEFAULT_OUT_BUFFER_SIZE (4096) +#define CLIENT_TIMEOUT_DEFAULT (60) +#define CLIENT_MAX_CONNECTIONS_DEFAULT (10) +#define CLIENT_MAX_COMMAND_LIST_DEFAULT (2048*1024) +#define CLIENT_MAX_OUTPUT_BUFFER_SIZE_DEFAULT (8192*1024) + +/* set this to zero to indicate we have no possible clients */ +static unsigned int client_max_connections; /*CLIENT_MAX_CONNECTIONS_DEFAULT; */ +static int client_timeout = CLIENT_TIMEOUT_DEFAULT; +static size_t client_max_command_list_size = + CLIENT_MAX_COMMAND_LIST_DEFAULT; +static size_t client_max_output_buffer_size = + CLIENT_MAX_OUTPUT_BUFFER_SIZE_DEFAULT; + +/* maybe make conf option for this, or... 32 might be good enough */ +static long int client_list_cache_size = 32; + +/* shared globally between all clients: */ +static struct strnode *list_cache; +static struct strnode *list_cache_head; +static struct strnode *list_cache_tail; + +struct client { + struct list_head siblings; + + char buffer[CLIENT_MAX_BUFFER_LENGTH]; + size_t bufferLength; + size_t bufferPos; + int fd; /* file descriptor; -1 if expired */ + int permission; + time_t lastTime; + struct strnode *cmd_list; /* for when in list mode */ + struct strnode *cmd_list_tail; /* for when in list mode */ + int cmd_list_OK; /* print OK after each command execution */ + size_t cmd_list_size; /* mem cmd_list consumes */ + int cmd_list_dup; /* has the cmd_list been copied to private space? */ + struct sllnode *deferred_send; /* for output if client is slow */ + size_t deferred_bytes; /* mem deferred_send consumes */ + unsigned int num; /* client number */ + + char *send_buf; + size_t send_buf_used; /* bytes used this instance */ + size_t send_buf_size; /* bytes usable this instance */ + size_t send_buf_alloc; /* bytes actually allocated */ +}; + +static LIST_HEAD(clients); +static unsigned num_clients; + +static void client_write_deferred(struct client *client); + +static void client_write_output(struct client *client); + +#ifdef SO_SNDBUF +static size_t get_default_snd_buf_size(struct client *client) +{ + int new_size; + socklen_t sockOptLen = sizeof(int); + + if (getsockopt(client->fd, SOL_SOCKET, SO_SNDBUF, + (char *)&new_size, &sockOptLen) < 0) { + DEBUG("problem getting sockets send buffer size\n"); + return CLIENT_DEFAULT_OUT_BUFFER_SIZE; + } + if (new_size > 0) + return (size_t)new_size; + DEBUG("sockets send buffer size is not positive\n"); + return CLIENT_DEFAULT_OUT_BUFFER_SIZE; +} +#else /* !SO_SNDBUF */ +static size_t get_default_snd_buf_size(struct client *client) +{ + return CLIENT_DEFAULT_OUT_BUFFER_SIZE; +} +#endif /* !SO_SNDBUF */ + +static void set_send_buf_size(struct client *client) +{ + size_t new_size = get_default_snd_buf_size(client); + if (client->send_buf_size != new_size) { + client->send_buf_size = new_size; + /* don't resize to get smaller, only bigger */ + if (client->send_buf_alloc < new_size) { + if (client->send_buf) + free(client->send_buf); + client->send_buf = xmalloc(new_size); + client->send_buf_alloc = new_size; + } + } +} + +static inline int client_is_expired(const struct client *client) +{ + return client->fd < 0; +} + +static int global_expired; + +static inline void client_set_expired(struct client *client) +{ + if (client->fd >= 0) { + xclose(client->fd); + client->fd = -1; + } + + global_expired = 1; +} + +static void client_init(struct client *client, int fd) +{ + static unsigned int next_client_num; + + assert(fd >= 0); + + client->cmd_list_size = 0; + client->cmd_list_dup = 0; + client->cmd_list_OK = -1; + client->bufferLength = 0; + client->bufferPos = 0; + client->fd = fd; + set_nonblocking(fd); + client->lastTime = time(NULL); + client->cmd_list = NULL; + client->cmd_list_tail = NULL; + client->deferred_send = NULL; + client->deferred_bytes = 0; + client->num = next_client_num++; + client->send_buf_used = 0; + + client->permission = getDefaultPermissions(); + set_send_buf_size(client); + + xwrite(fd, GREETING, strlen(GREETING)); +} + +static void free_cmd_list(struct strnode *list) +{ + struct strnode *tmp = list; + + while (tmp) { + struct strnode *next = tmp->next; + if (tmp >= list_cache_head && tmp <= list_cache_tail) { + /* inside list_cache[] array */ + tmp->data = NULL; + tmp->next = NULL; + } else + free(tmp); + tmp = next; + } +} + +static void cmd_list_clone(struct client *client) +{ + struct strnode *new = dup_strlist(client->cmd_list); + free_cmd_list(client->cmd_list); + client->cmd_list = new; + client->cmd_list_dup = 1; + + /* new tail */ + while (new && new->next) + new = new->next; + client->cmd_list_tail = new; +} + +static void new_cmd_list_ptr(struct client *client, char *s, const int size) +{ + int i; + struct strnode *new; + + if (!client->cmd_list_dup) { + for (i = client_list_cache_size - 1; i >= 0; --i) { + if (list_cache[i].data) + continue; + new = &(list_cache[i]); + new->data = s; + /* implied in free_cmd_list() and init: */ + /* last->next->next = NULL; */ + goto out; + } + } + + /* allocate from the heap */ + new = client->cmd_list_dup ? new_strnode_dup(s, size) + : new_strnode(s); +out: + if (client->cmd_list) { + client->cmd_list_tail->next = new; + client->cmd_list_tail = new; + } else + client->cmd_list = client->cmd_list_tail = new; +} + +static void client_close(struct client *client) +{ + struct sllnode *buf; + + assert(client->fd >= 0); + + xclose(client->fd); + + assert(num_clients > 0); + assert(!list_empty(&clients)); + list_del(&client->siblings); + --num_clients; + + if (client->cmd_list) { + free_cmd_list(client->cmd_list); + client->cmd_list = NULL; + } + + if ((buf = client->deferred_send)) { + do { + struct sllnode *prev = buf; + buf = buf->next; + free(prev); + } while (buf); + client->deferred_send = NULL; + } + + if (client->send_buf) + free(client->send_buf); + + SECURE("client %i: closed\n", client->num); + free(client); +} + +static const char * +sockaddr_to_tmp_string(const struct sockaddr *addr) +{ + const char *hostname; + + switch (addr->sa_family) { +#ifdef HAVE_TCP + case AF_INET: + hostname = (const char *)inet_ntoa(((const struct sockaddr_in *) + addr)->sin_addr); + if (!hostname) + hostname = "error getting ipv4 address"; + break; +#ifdef HAVE_IPV6 + case AF_INET6: + { + static char host[INET6_ADDRSTRLEN + 1]; + memset(host, 0, INET6_ADDRSTRLEN + 1); + if (inet_ntop(AF_INET6, (const void *) + &(((const struct sockaddr_in6 *)addr)-> + sin6_addr), host, + INET6_ADDRSTRLEN)) { + hostname = (const char *)host; + } else { + hostname = "error getting ipv6 address"; + } + } + break; +#endif +#endif /* HAVE_TCP */ +#ifdef HAVE_UN + case AF_UNIX: + hostname = "local connection"; + break; +#endif /* HAVE_UN */ + default: + hostname = "unknown"; + } + + return hostname; +} + +void client_new(int fd, const struct sockaddr *addr) +{ + struct client *client; + + if (num_clients >= client_max_connections) { + ERROR("Max Connections Reached!\n"); + xclose(fd); + return; + } + + client = xcalloc(1, sizeof(*client)); + list_add(&client->siblings, &clients); + ++num_clients; + client_init(client, fd); + SECURE("client %i: opened from %s\n", client->num, + sockaddr_to_tmp_string(addr)); +} + +static int client_process_line(struct client *client) +{ + int ret = 1; + char *line = client->buffer + client->bufferPos; + + if (client->cmd_list_OK >= 0) { + if (strcmp(line, CLIENT_LIST_MODE_END) == 0) { + DEBUG("client %i: process command " + "list\n", client->num); + + global_expired = 0; + ret = processListOfCommands(client->fd, + &(client->permission), + &global_expired, + client->cmd_list_OK, + client->cmd_list); + DEBUG("client %i: process command " + "list returned %i\n", client->num, ret); + + if (ret == COMMAND_RETURN_CLOSE || + client_is_expired(client)) { + client_close(client); + return COMMAND_RETURN_CLOSE; + } + + if (ret == 0) + commandSuccess(client->fd); + + client_write_output(client); + free_cmd_list(client->cmd_list); + client->cmd_list = NULL; + client->cmd_list_OK = -1; + } else { + size_t len = strlen(line) + 1; + client->cmd_list_size += len; + if (client->cmd_list_size > + client_max_command_list_size) { + ERROR("client %i: command " + "list size (%lu) is " + "larger than the max " + "(%lu)\n", + client->num, + (unsigned long)client->cmd_list_size, + (unsigned long) + client_max_command_list_size); + client_close(client); + ret = COMMAND_RETURN_CLOSE; + } else + new_cmd_list_ptr(client, line, len); + } + } else { + if (strcmp(line, CLIENT_LIST_MODE_BEGIN) == 0) { + client->cmd_list_OK = 0; + ret = 1; + } else if (strcmp(line, CLIENT_LIST_OK_MODE_BEGIN) == 0) { + client->cmd_list_OK = 1; + ret = 1; + } else { + DEBUG("client %i: process command \"%s\"\n", + client->num, line); + ret = processCommand(client->fd, + &(client->permission), line); + DEBUG("client %i: command returned %i\n", + client->num, ret); + + if (ret == COMMAND_RETURN_CLOSE || + client_is_expired(client)) { + client_close(client); + return COMMAND_RETURN_CLOSE; + } + + if (ret == 0) + commandSuccess(client->fd); + + client_write_output(client); + } + } + + return ret; +} + +static int client_input_received(struct client *client, int bytesRead) +{ + int ret = 0; + char *buf_tail = &(client->buffer[client->bufferLength - 1]); + + while (bytesRead > 0) { + client->bufferLength++; + bytesRead--; + buf_tail++; + if (*buf_tail == '\n') { + *buf_tail = '\0'; + if (client->bufferLength > client->bufferPos) { + if (*(buf_tail - 1) == '\r') + *(buf_tail - 1) = '\0'; + } + ret = client_process_line(client); + if (client_is_expired(client)) + return ret; + client->bufferPos = client->bufferLength; + } + if (client->bufferLength == CLIENT_MAX_BUFFER_LENGTH) { + if (client->bufferPos == 0) { + ERROR("client %i: buffer overflow\n", + client->num); + client_close(client); + return 1; + } + if (client->cmd_list_OK >= 0 && + client->cmd_list && + !client->cmd_list_dup) + cmd_list_clone(client); + assert(client->bufferLength >= client->bufferPos + && "bufferLength >= bufferPos"); + client->bufferLength -= client->bufferPos; + memmove(client->buffer, + client->buffer + client->bufferPos, + client->bufferLength); + client->bufferPos = 0; + } + if (ret == COMMAND_RETURN_KILL || ret == COMMAND_RETURN_CLOSE) { + return ret; + } + + } + + return ret; +} + +static int client_read(struct client *client) +{ + int bytesRead; + + bytesRead = read(client->fd, + client->buffer + client->bufferLength, + CLIENT_MAX_BUFFER_LENGTH - client->bufferLength); + + if (bytesRead > 0) + return client_input_received(client, bytesRead); + else if (bytesRead == 0 || (bytesRead < 0 && errno != EINTR)) { + client_close(client); + } else + return 0; + + return 1; +} + +static void client_manager_register_read_fd(fd_set * fds, int *fdmax) +{ + struct client *client; + + FD_ZERO(fds); + addListenSocketsToFdSet(fds, fdmax); + + list_for_each_entry(client, &clients, siblings) { + if (!client_is_expired(client) && !client->deferred_send) { + FD_SET(client->fd, fds); + if (*fdmax < client->fd) + *fdmax = client->fd; + } + } +} + +static void client_manager_register_write_fd(fd_set * fds, int *fdmax) +{ + struct client *client; + + FD_ZERO(fds); + + list_for_each_entry(client, &clients, siblings) { + if (client->fd >= 0 && !client_is_expired(client) + && client->deferred_send) { + FD_SET(client->fd, fds); + if (*fdmax < client->fd) + *fdmax = client->fd; + } + } +} + +int client_manager_io(void) +{ + fd_set rfds; + fd_set wfds; + fd_set efds; + struct client *client, *n; + int selret; + int fdmax = 0; + + FD_ZERO( &efds ); + client_manager_register_read_fd(&rfds, &fdmax); + client_manager_register_write_fd(&wfds, &fdmax); + + registered_IO_add_fds(&fdmax, &rfds, &wfds, &efds); + + selret = select(fdmax + 1, &rfds, &wfds, &efds, NULL); + if (selret < 0) { + if (errno == EINTR) + return 0; + + FATAL("select() failed: %s\n", strerror(errno)); + } + + registered_IO_consume_fds(&selret, &rfds, &wfds, &efds); + + getConnections(&rfds); + + list_for_each_entry_safe(client, n, &clients, siblings) { + if (FD_ISSET(client->fd, &rfds)) { + if (COMMAND_RETURN_KILL == + client_read(client)) { + return COMMAND_RETURN_KILL; + } + client->lastTime = time(NULL); + } + if (FD_ISSET(client->fd, &wfds)) { + client_write_deferred(client); + client->lastTime = time(NULL); + } + } + + return 0; +} + +void client_manager_init(void) +{ + char *test; + ConfigParam *param; + + param = getConfigParam(CONF_CONN_TIMEOUT); + + if (param) { + client_timeout = strtol(param->value, &test, 10); + if (*test != '\0' || client_timeout <= 0) { + FATAL("connection timeout \"%s\" is not a positive " + "integer, line %i\n", CONF_CONN_TIMEOUT, + param->line); + } + } + + param = getConfigParam(CONF_MAX_CONN); + + if (param) { + client_max_connections = strtol(param->value, &test, 10); + if (*test != '\0' || client_max_connections <= 0) { + FATAL("max connections \"%s\" is not a positive integer" + ", line %i\n", param->value, param->line); + } + } else + client_max_connections = CLIENT_MAX_CONNECTIONS_DEFAULT; + + param = getConfigParam(CONF_MAX_COMMAND_LIST_SIZE); + + if (param) { + long tmp = strtol(param->value, &test, 10); + if (*test != '\0' || tmp <= 0) { + FATAL("max command list size \"%s\" is not a positive " + "integer, line %i\n", param->value, param->line); + } + client_max_command_list_size = tmp * 1024; + } + + param = getConfigParam(CONF_MAX_OUTPUT_BUFFER_SIZE); + + if (param) { + long tmp = strtol(param->value, &test, 10); + if (*test != '\0' || tmp <= 0) { + FATAL("max output buffer size \"%s\" is not a positive " + "integer, line %i\n", param->value, param->line); + } + client_max_output_buffer_size = tmp * 1024; + } + + list_cache = xcalloc(client_list_cache_size, sizeof(struct strnode)); + list_cache_head = &(list_cache[0]); + list_cache_tail = &(list_cache[client_list_cache_size - 1]); +} + +static void client_close_all(void) +{ + struct client *client, *n; + + list_for_each_entry_safe(client, n, &clients, siblings) + client_close(client); + num_clients = 0; + + free(list_cache); +} + +void client_manager_deinit(void) +{ + client_close_all(); + + client_max_connections = 0; +} + +void client_manager_expire(void) +{ + struct client *client, *n; + + list_for_each_entry_safe(client, n, &clients, siblings) { + if (client_is_expired(client)) { + DEBUG("client %i: expired\n", client->num); + client_close(client); + } else if (time(NULL) - client->lastTime > + client_timeout) { + DEBUG("client %i: timeout\n", client->num); + client_close(client); + } + } +} + +static void client_write_deferred(struct client *client) +{ + struct sllnode *buf; + ssize_t ret = 0; + + buf = client->deferred_send; + while (buf) { + assert(buf->size > 0); + + ret = write(client->fd, buf->data, buf->size); + if (ret < 0) + break; + else if ((size_t)ret < buf->size) { + assert(client->deferred_bytes >= (size_t)ret); + client->deferred_bytes -= ret; + buf->data = (char *)buf->data + ret; + buf->size -= ret; + } else { + struct sllnode *tmp = buf; + size_t decr = (buf->size + sizeof(struct sllnode)); + + assert(client->deferred_bytes >= decr); + client->deferred_bytes -= decr; + buf = buf->next; + free(tmp); + client->deferred_send = buf; + } + client->lastTime = time(NULL); + } + + if (!client->deferred_send) { + DEBUG("client %i: buffer empty %lu\n", client->num, + (unsigned long)client->deferred_bytes); + assert(client->deferred_bytes == 0); + } else if (ret < 0 && errno != EAGAIN && errno != EINTR) { + /* cause client to close */ + DEBUG("client %i: problems flushing buffer\n", + client->num); + client_set_expired(client); + } +} + +static struct client *client_by_fd(int fd) +{ + struct client *client; + + list_for_each_entry(client, &clients, siblings) + if (client->fd == fd) + return client; + + return NULL; +} + +int client_print(int fd, const char *buffer, size_t buflen) +{ + size_t copylen; + struct client *client; + + assert(fd >= 0); + + client = client_by_fd(fd); + if (client == NULL) + return -1; + + /* if fd isn't found or client is going to be closed, do nothing */ + if (client_is_expired(client)) + return 0; + + while (buflen > 0 && !client_is_expired(client)) { + size_t left; + + assert(client->send_buf_size >= client->send_buf_used); + left = client->send_buf_size - client->send_buf_used; + + copylen = buflen > left ? left : buflen; + memcpy(client->send_buf + client->send_buf_used, buffer, + copylen); + buflen -= copylen; + client->send_buf_used += copylen; + buffer += copylen; + if (client->send_buf_used >= client->send_buf_size) + client_write_output(client); + } + + return 0; +} + +static void client_defer_output(struct client *client, + const void *data, size_t length) +{ + struct sllnode **buf_r; + + assert(length > 0); + + client->deferred_bytes += sizeof(struct sllnode) + length; + if (client->deferred_bytes > client_max_output_buffer_size) { + ERROR("client %i: output buffer size (%lu) is " + "larger than the max (%lu)\n", + client->num, + (unsigned long)client->deferred_bytes, + (unsigned long)client_max_output_buffer_size); + /* cause client to close */ + client_set_expired(client); + return; + } + + buf_r = &client->deferred_send; + while (*buf_r != NULL) + buf_r = &(*buf_r)->next; + *buf_r = new_sllnode(data, length); +} + +static void client_write(struct client *client, + const char *data, size_t length) +{ + ssize_t ret; + + assert(length > 0); + assert(client->deferred_send == NULL); + + if ((ret = write(client->fd, data, length)) < 0) { + if (errno == EAGAIN || errno == EINTR) { + client_defer_output(client, data, length); + } else { + DEBUG("client %i: problems writing\n", client->num); + client_set_expired(client); + return; + } + } else if ((size_t)ret < client->send_buf_used) { + client_defer_output(client, data + ret, length - ret); + } + + if (client->deferred_send) + DEBUG("client %i: buffer created\n", client->num); +} + +static void client_write_output(struct client *client) +{ + if (client_is_expired(client) || !client->send_buf_used) + return; + + if (client->deferred_send != NULL) + client_defer_output(client, client->send_buf, + client->send_buf_used); + else + client_write(client, client->send_buf, client->send_buf_used); + + client->send_buf_used = 0; +} + |