aboutsummaryrefslogtreecommitdiffstats
path: root/src/output/httpd_client.c
diff options
context:
space:
mode:
authorMax Kellermann <max@duempel.org>2009-03-15 03:32:34 +0100
committerMax Kellermann <max@duempel.org>2009-03-15 03:32:34 +0100
commite62580db0bab0650f088f9f45c66657d18dfb679 (patch)
tree19c0deec7b75ea7fe2513c674c7de3fe2647fa7c /src/output/httpd_client.c
parent565afefc66b7753ed27f6b7febaf32667c38c44a (diff)
downloadmpd-e62580db0bab0650f088f9f45c66657d18dfb679.tar.gz
mpd-e62580db0bab0650f088f9f45c66657d18dfb679.tar.xz
mpd-e62580db0bab0650f088f9f45c66657d18dfb679.zip
httpd: new output plugin to replace "shout"
Let's get rid of the "shout" plugin, and the awfully complicated icecast daemon setup! MPD can do better if it's doing the HTTP server stuff on its own. This new plugin has several advantages: - easier to set up - only one daemon, no password settings, no mount settings - MPD controls the encoder and thus already knows the packet boundaries - icecast has to parse them - MPD doesn't bother to encode data while nobody is listening This implementation is very experimental (no header parsing, ignores request URI, no icy-metadata, ...). It should be able to suport several encoders in parallel in the future (with different bit rates, different codec, ...), to make MPD the perfect streaming server. Once MPD gets multi-player support, we can even mount several different radio stations on one server.
Diffstat (limited to '')
-rw-r--r--src/output/httpd_client.c523
1 files changed, 523 insertions, 0 deletions
diff --git a/src/output/httpd_client.c b/src/output/httpd_client.c
new file mode 100644
index 000000000..fb3f0548a
--- /dev/null
+++ b/src/output/httpd_client.c
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2003-2009 The Music Player Daemon Project
+ * 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.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "httpd_client.h"
+#include "httpd_internal.h"
+#include "fifo_buffer.h"
+#include "page.h"
+
+#include <stdbool.h>
+#include <assert.h>
+#include <string.h>
+
+struct httpd_client {
+ /**
+ * The httpd output object this client is connected to.
+ */
+ struct httpd_output *httpd;
+
+ /**
+ * The TCP socket.
+ */
+ GIOChannel *channel;
+
+ /**
+ * The GLib main loop source id for reading from the socket,
+ * and to detect errors.
+ */
+ guint read_source_id;
+
+ /**
+ * The GLib main loop source id for writing to the socket. If
+ * 0, then there is no event source currently (because there
+ * are no queued pages).
+ */
+ guint write_source_id;
+
+ /**
+ * For buffered reading. This pointer is only valid while the
+ * HTTP request is read.
+ */
+ struct fifo_buffer *input;
+
+ /**
+ * The current state of the client.
+ */
+ enum {
+ /** reading the request line */
+ REQUEST,
+
+ /** reading the request headers */
+ HEADERS,
+
+ /** sending the HTTP response */
+ RESPONSE,
+ } state;
+
+ /**
+ * A queue of #page objects to be sent to the client.
+ */
+ GQueue *pages;
+
+ /**
+ * The #page which is currently being sent to the client.
+ */
+ struct page *current_page;
+
+ /**
+ * The amount of bytes which were already sent from
+ * #current_page.
+ */
+ size_t current_position;
+};
+
+static void
+httpd_client_unref_page(gpointer data, G_GNUC_UNUSED gpointer user_data)
+{
+ struct page *page = data;
+
+ page_unref(page);
+}
+
+void
+httpd_client_free(struct httpd_client *client)
+{
+ if (client->state == RESPONSE) {
+ if (client->write_source_id != 0)
+ g_source_remove(client->write_source_id);
+
+ if (client->current_page != NULL)
+ page_unref(client->current_page);
+
+ g_queue_foreach(client->pages, httpd_client_unref_page, NULL);
+ g_queue_free(client->pages);
+ } else
+ fifo_buffer_free(client->input);
+
+ g_source_remove(client->read_source_id);
+ g_io_channel_unref(client->channel);
+ g_free(client);
+}
+
+/**
+ * Frees the client and removes it from the server's client list.
+ */
+static void
+httpd_client_close(struct httpd_client *client)
+{
+ httpd_output_remove_client(client->httpd, client);
+ httpd_client_free(client);
+}
+
+/**
+ * Switch the client to the "RESPONSE" state.
+ */
+static void
+httpd_client_begin_response(struct httpd_client *client)
+{
+ client->state = RESPONSE;
+ client->write_source_id = 0;
+ client->pages = g_queue_new();
+ client->current_page = NULL;
+
+ httpd_output_send_header(client->httpd, client);
+}
+
+/**
+ * Handle a line of the HTTP request.
+ */
+static bool
+httpd_client_handle_line(struct httpd_client *client, const char *line)
+{
+ assert(client->state != RESPONSE);
+
+ if (client->state == REQUEST) {
+ if (strncmp(line, "GET /", 5) != 0) {
+ /* only GET is supported */
+ g_warning("malformed request line from client");
+ return false;
+ }
+
+ line = strchr(line + 5, ' ');
+ if (line == NULL || strncmp(line + 1, "HTTP/", 5) != 0) {
+ /* HTTP/0.9 without request headers */
+ httpd_client_begin_response(client);
+ return true;
+ }
+
+ /* after the request line, request headers follow */
+ client->state = HEADERS;
+ return true;
+ } else {
+ if (*line == 0) {
+ /* empty line: request is finished */
+ httpd_client_begin_response(client);
+ return true;
+ }
+
+ /* expect more request headers */
+ return true;
+ }
+}
+
+/**
+ * Check if a complete line of input is present in the input buffer,
+ * and duplicates it. It is removed from the input buffer. The
+ * return value has to be freed with g_free().
+ */
+static char *
+httpd_client_read_line(struct httpd_client *client)
+{
+ const char *p, *newline;
+ size_t length;
+ char *line;
+
+ p = fifo_buffer_read(client->input, &length);
+ if (p == NULL)
+ /* empty input buffer */
+ return NULL;
+
+ newline = memchr(p, '\n', length);
+ if (newline == NULL)
+ /* incomplete line */
+ return NULL;
+
+ line = g_strndup(p, newline - p);
+ fifo_buffer_consume(client->input, newline - p + 1);
+
+ /* remove trailing whitespace (e.g. '\r') */
+ return g_strchomp(line);
+}
+
+/**
+ * Sends the status line and response headers to the client.
+ */
+static bool
+httpd_client_send_response(struct httpd_client *client)
+{
+ char buffer[1024];
+ GError *error = NULL;
+ GIOStatus status;
+ gsize bytes_written;
+
+ assert(client->state == RESPONSE);
+
+ g_snprintf(buffer, sizeof(buffer),
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Type: %s\r\n"
+ "Connection: close\r\n"
+ "Pragma: no-cache\r\n"
+ "Cache-Control: no-cache, no-store\r\n"
+ "\r\n",
+ client->httpd->content_type);
+
+ status = g_io_channel_write_chars(client->channel,
+ buffer, strlen(buffer),
+ &bytes_written, &error);
+ switch (status) {
+ case G_IO_STATUS_NORMAL:
+ case G_IO_STATUS_AGAIN:
+ return true;
+
+ case G_IO_STATUS_EOF:
+ /* client has disconnected */
+
+ httpd_client_close(client);
+ return false;
+
+ case G_IO_STATUS_ERROR:
+ /* I/O error */
+
+ g_warning("failed to write to client: %s", error->message);
+ g_error_free(error);
+
+ httpd_client_close(client);
+ return false;
+ }
+
+ /* unreachable */
+ httpd_client_close(client);
+ return false;
+}
+
+/**
+ * Data has been received from the client and it is appended to the
+ * input buffer.
+ */
+static bool
+httpd_client_received(struct httpd_client *client)
+{
+ char *line;
+ bool success;
+
+ while ((line = httpd_client_read_line(client)) != NULL) {
+ success = httpd_client_handle_line(client, line);
+ g_free(line);
+ if (!success)
+ return false;
+
+ if (client->state == RESPONSE) {
+ if (!fifo_buffer_is_empty(client->input)) {
+ g_warning("unexpected input from client");
+ return false;
+ }
+
+ fifo_buffer_free(client->input);
+
+ return httpd_client_send_response(client);
+ }
+ }
+
+ return true;
+}
+
+static bool
+httpd_client_read(struct httpd_client *client)
+{
+ char *p;
+ size_t max_length;
+ GError *error = NULL;
+ GIOStatus status;
+ gsize bytes_read;
+
+ if (client->state == RESPONSE) {
+ /* the client has already sent the request, and he
+ must not send more */
+ g_warning("unexpected input from client");
+ return false;
+ }
+
+ p = fifo_buffer_write(client->input, &max_length);
+ if (p == NULL) {
+ g_warning("buffer overflow");
+ return false;
+ }
+
+ status = g_io_channel_read_chars(client->channel, p, max_length,
+ &bytes_read, &error);
+ switch (status) {
+ case G_IO_STATUS_NORMAL:
+ fifo_buffer_append(client->input, bytes_read);
+ return httpd_client_received(client);
+
+ case G_IO_STATUS_AGAIN:
+ /* try again later, after select() */
+ return true;
+
+ case G_IO_STATUS_EOF:
+ /* peer disconnected */
+ return false;
+
+ case G_IO_STATUS_ERROR:
+ /* I/O error */
+ g_warning("failed to read from client: %s",
+ error->message);
+ g_error_free(error);
+ return false;
+ }
+
+ /* unreachable */
+ return false;
+}
+
+static gboolean
+httpd_client_in_event(G_GNUC_UNUSED GIOChannel *source, GIOCondition condition,
+ gpointer data)
+{
+ struct httpd_client *client = data;
+ struct httpd_output *httpd = client->httpd;
+ bool ret;
+
+ g_mutex_lock(httpd->mutex);
+
+ if (condition == G_IO_IN && httpd_client_read(client)) {
+ ret = true;
+ } else {
+ httpd_client_close(client);
+ ret = false;
+ }
+
+ g_mutex_unlock(httpd->mutex);
+
+ return ret;
+}
+
+struct httpd_client *
+httpd_client_new(struct httpd_output *httpd, int fd)
+{
+ struct httpd_client *client = g_new(struct httpd_client, 1);
+
+ client->httpd = httpd;
+
+#ifndef G_OS_WIN32
+ client->channel = g_io_channel_unix_new(fd);
+#else
+ client->channel = g_io_channel_win32_new_socket(fd);
+#endif
+
+ /* GLib is responsible for closing the file descriptor */
+ g_io_channel_set_close_on_unref(client->channel, true);
+ /* NULL encoding means the stream is binary safe */
+ g_io_channel_set_encoding(client->channel, NULL, NULL);
+ /* we prefer to do buffering */
+ g_io_channel_set_buffered(client->channel, false);
+
+ client->read_source_id = g_io_add_watch(client->channel,
+ G_IO_IN|G_IO_ERR|G_IO_HUP,
+ httpd_client_in_event, client);
+
+ client->input = fifo_buffer_new(4096);
+ client->state = REQUEST;
+
+ return client;
+}
+
+static void
+httpd_client_add_page_size(gpointer data, gpointer user_data)
+{
+ struct page *page = data;
+ size_t *size = user_data;
+
+ *size += page->size;
+}
+
+size_t
+httpd_client_queue_size(const struct httpd_client *client)
+{
+ size_t size = 0;
+
+ if (client->state != RESPONSE)
+ return 0;
+
+ g_queue_foreach(client->pages, httpd_client_add_page_size, &size);
+ return size;
+}
+
+void
+httpd_client_cancel(const struct httpd_client *client)
+{
+ if (client->state != RESPONSE)
+ return;
+
+ g_queue_foreach(client->pages, httpd_client_unref_page, NULL);
+}
+
+static GIOStatus
+write_page_to_channel(GIOChannel *channel,
+ const struct page *page, size_t position,
+ gsize *bytes_written_r, GError **error)
+{
+ assert(channel != NULL);
+ assert(page != NULL);
+ assert(position < page->size);
+
+ return g_io_channel_write_chars(channel,
+ (const gchar*)page->data + position,
+ page->size - position,
+ bytes_written_r, error);
+}
+
+static gboolean
+httpd_client_out_event(GIOChannel *source,
+ G_GNUC_UNUSED GIOCondition condition, gpointer data)
+{
+ struct httpd_client *client = data;
+ struct httpd_output *httpd = client->httpd;
+ GError *error = NULL;
+ GIOStatus status;
+ gsize bytes_written;
+
+ g_mutex_lock(httpd->mutex);
+
+ assert(condition == G_IO_OUT);
+ assert(client->state == RESPONSE);
+
+ if (client->current_page == NULL) {
+ client->current_page = g_queue_pop_head(client->pages);
+ client->current_position = 0;
+ }
+
+ status = write_page_to_channel(source, client->current_page,
+ client->current_position,
+ &bytes_written, &error);
+ switch (status) {
+ case G_IO_STATUS_NORMAL:
+ client->current_position += bytes_written;
+ assert(client->current_position <= client->current_page->size);
+
+ if (client->current_position >= client->current_page->size) {
+ page_unref(client->current_page);
+ client->current_page = NULL;
+
+ if (g_queue_is_empty(client->pages)) {
+ /* all pages are sent: remove the
+ event source */
+ client->write_source_id = 0;
+
+ g_mutex_unlock(httpd->mutex);
+ return false;
+ }
+ }
+
+ g_mutex_unlock(httpd->mutex);
+ return true;
+
+ case G_IO_STATUS_AGAIN:
+ g_mutex_unlock(httpd->mutex);
+ return true;
+
+ case G_IO_STATUS_EOF:
+ /* client has disconnected */
+
+ httpd_client_close(client);
+ g_mutex_unlock(httpd->mutex);
+ return false;
+
+ case G_IO_STATUS_ERROR:
+ /* I/O error */
+
+ g_warning("failed to write to client: %s", error->message);
+ g_error_free(error);
+
+ httpd_client_close(client);
+ g_mutex_unlock(httpd->mutex);
+ return false;
+ }
+
+ /* unreachable */
+ httpd_client_close(client);
+ g_mutex_unlock(httpd->mutex);
+ return false;
+}
+
+void
+httpd_client_send(struct httpd_client *client, struct page *page)
+{
+ if (client->state != RESPONSE)
+ /* the client is still writing the HTTP request */
+ return;
+
+ page_ref(page);
+ g_queue_push_tail(client->pages, page);
+
+ if (client->write_source_id == 0)
+ client->write_source_id =
+ g_io_add_watch(client->channel, G_IO_OUT,
+ httpd_client_out_event, client);
+}