From 0bbfb28992d0684a59fd518a4e16333d82bcf716 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Wed, 19 Feb 2014 09:08:29 +0100 Subject: output/httpd: move to dedicated directory --- src/IcyMetaDataServer.cxx | 134 ------ src/IcyMetaDataServer.hxx | 39 -- src/Page.cxx | 70 --- src/Page.hxx | 102 ----- src/output/Registry.cxx | 2 +- src/output/plugins/HttpdClient.cxx | 485 -------------------- src/output/plugins/HttpdClient.hxx | 193 -------- src/output/plugins/HttpdInternal.hxx | 273 ----------- src/output/plugins/HttpdOutputPlugin.cxx | 601 ------------------------- src/output/plugins/HttpdOutputPlugin.hxx | 25 - src/output/plugins/httpd/HttpdClient.cxx | 485 ++++++++++++++++++++ src/output/plugins/httpd/HttpdClient.hxx | 193 ++++++++ src/output/plugins/httpd/HttpdInternal.hxx | 273 +++++++++++ src/output/plugins/httpd/HttpdOutputPlugin.cxx | 601 +++++++++++++++++++++++++ src/output/plugins/httpd/HttpdOutputPlugin.hxx | 25 + src/output/plugins/httpd/IcyMetaDataServer.cxx | 134 ++++++ src/output/plugins/httpd/IcyMetaDataServer.hxx | 39 ++ src/output/plugins/httpd/Page.cxx | 70 +++ src/output/plugins/httpd/Page.hxx | 102 +++++ 19 files changed, 1923 insertions(+), 1923 deletions(-) delete mode 100644 src/IcyMetaDataServer.cxx delete mode 100644 src/IcyMetaDataServer.hxx delete mode 100644 src/Page.cxx delete mode 100644 src/Page.hxx delete mode 100644 src/output/plugins/HttpdClient.cxx delete mode 100644 src/output/plugins/HttpdClient.hxx delete mode 100644 src/output/plugins/HttpdInternal.hxx delete mode 100644 src/output/plugins/HttpdOutputPlugin.cxx delete mode 100644 src/output/plugins/HttpdOutputPlugin.hxx create mode 100644 src/output/plugins/httpd/HttpdClient.cxx create mode 100644 src/output/plugins/httpd/HttpdClient.hxx create mode 100644 src/output/plugins/httpd/HttpdInternal.hxx create mode 100644 src/output/plugins/httpd/HttpdOutputPlugin.cxx create mode 100644 src/output/plugins/httpd/HttpdOutputPlugin.hxx create mode 100644 src/output/plugins/httpd/IcyMetaDataServer.cxx create mode 100644 src/output/plugins/httpd/IcyMetaDataServer.hxx create mode 100644 src/output/plugins/httpd/Page.cxx create mode 100644 src/output/plugins/httpd/Page.hxx (limited to 'src') diff --git a/src/IcyMetaDataServer.cxx b/src/IcyMetaDataServer.cxx deleted file mode 100644 index 146df23d1..000000000 --- a/src/IcyMetaDataServer.cxx +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2003-2014 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 "config.h" -#include "IcyMetaDataServer.hxx" -#include "Page.hxx" -#include "tag/Tag.hxx" -#include "util/FormatString.hxx" - -#include - -#include - -char* -icy_server_metadata_header(const char *name, - const char *genre, const char *url, - const char *content_type, int metaint) -{ - return FormatNew("ICY 200 OK\r\n" - "icy-notice1:
This stream requires an audio player!
\r\n" /* TODO */ - "icy-notice2:MPD - The music player daemon
\r\n" - "icy-name: %s\r\n" /* TODO */ - "icy-genre: %s\r\n" /* TODO */ - "icy-url: %s\r\n" /* TODO */ - "icy-pub:1\r\n" - "icy-metaint:%d\r\n" - /* TODO "icy-br:%d\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", - name, - genre, - url, - metaint, - /* bitrate, */ - content_type); -} - -static char * -icy_server_metadata_string(const char *stream_title, const char* stream_url) -{ - gchar *icy_metadata; - guint meta_length; - - // The leading n is a placeholder for the length information - icy_metadata = FormatNew("nStreamTitle='%s';" - "StreamUrl='%s';", - stream_title, - stream_url); - - meta_length = strlen(icy_metadata); - - meta_length--; // subtract placeholder - - meta_length = ((int)meta_length / 16) + 1; - - icy_metadata[0] = meta_length; - - if (meta_length > 255) { - delete[] icy_metadata; - return nullptr; - } - - return icy_metadata; -} - -Page * -icy_server_metadata_page(const Tag &tag, const TagType *types) -{ - const gchar *tag_items[TAG_NUM_OF_ITEM_TYPES]; - gint last_item, item; - guint position; - gchar *icy_string; - gchar stream_title[(1 + 255 - 28) * 16]; // Length + Metadata - - // "StreamTitle='';StreamUrl='';" - // = 4081 - 28 - stream_title[0] = '\0'; - - last_item = -1; - - while (*types != TAG_NUM_OF_ITEM_TYPES) { - const gchar *tag_item = tag.GetValue(*types++); - if (tag_item) - tag_items[++last_item] = tag_item; - } - - position = item = 0; - while (position < sizeof(stream_title) && item <= last_item) { - gint length = 0; - - length = g_strlcpy(stream_title + position, - tag_items[item++], - sizeof(stream_title) - position); - - position += length; - - if (item <= last_item) { - length = g_strlcpy(stream_title + position, - " - ", - sizeof(stream_title) - position); - - position += length; - } - } - - icy_string = icy_server_metadata_string(stream_title, ""); - - if (icy_string == nullptr) - return nullptr; - - Page *icy_metadata = Page::Copy(icy_string, (icy_string[0] * 16) + 1); - - delete[] icy_string; - - return icy_metadata; -} diff --git a/src/IcyMetaDataServer.hxx b/src/IcyMetaDataServer.hxx deleted file mode 100644 index 773b46641..000000000 --- a/src/IcyMetaDataServer.hxx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2003-2014 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. - */ - -#ifndef MPD_ICY_META_DATA_SERVER_HXX -#define MPD_ICY_META_DATA_SERVER_HXX - -#include "tag/TagType.h" - -struct Tag; -class Page; - -/** - * Free the return value with delete[]. - */ -char* -icy_server_metadata_header(const char *name, - const char *genre, const char *url, - const char *content_type, int metaint); - -Page * -icy_server_metadata_page(const Tag &tag, const TagType *types); - -#endif diff --git a/src/Page.cxx b/src/Page.cxx deleted file mode 100644 index e22134bbc..000000000 --- a/src/Page.cxx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2003-2014 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 "config.h" -#include "Page.hxx" -#include "util/Alloc.hxx" - -#include - -#include -#include -#include - -Page * -Page::Create(size_t size) -{ - void *p = xalloc(sizeof(Page) + size - - sizeof(Page::data)); - return ::new(p) Page(size); -} - -Page * -Page::Copy(const void *data, size_t size) -{ - assert(data != nullptr); - - Page *page = Create(size); - memcpy(page->data, data, size); - return page; -} - -Page * -Page::Concat(const Page &a, const Page &b) -{ - Page *page = Create(a.size + b.size); - - memcpy(page->data, a.data, a.size); - memcpy(page->data + a.size, b.data, b.size); - - return page; -} - -bool -Page::Unref() -{ - bool unused = ref.Decrement(); - - if (unused) { - this->Page::~Page(); - free(this); - } - - return unused; -} diff --git a/src/Page.hxx b/src/Page.hxx deleted file mode 100644 index 95f35d06a..000000000 --- a/src/Page.hxx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2003-2014 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. - */ - -/** \file - * - * This is a library which manages reference counted buffers. - */ - -#ifndef MPD_PAGE_HXX -#define MPD_PAGE_HXX - -#include "util/RefCount.hxx" - -#include - -/** - * A dynamically allocated buffer which keeps track of its reference - * count. This is useful for passing buffers around, when several - * instances hold references to one buffer. - */ -class Page { - /** - * The number of references to this buffer. This library uses - * atomic functions to access it, i.e. no locks are required. - * As soon as this attribute reaches zero, the buffer is - * freed. - */ - RefCount ref; - -public: - /** - * The size of this buffer in bytes. - */ - const size_t size; - - /** - * Dynamic array containing the buffer data. - */ - unsigned char data[sizeof(long)]; - -protected: - Page(size_t _size):size(_size) {} - ~Page() = default; - - /** - * Allocates a new #Page object, without filling the data - * element. - */ - static Page *Create(size_t size); - -public: - /** - * Creates a new #page object, and copies data from the - * specified buffer. It is initialized with a reference count - * of 1. - * - * @param data the source buffer - * @param size the size of the source buffer - */ - static Page *Copy(const void *data, size_t size); - - /** - * Concatenates two pages to a new page. - * - * @param a the first page - * @param b the second page, which is appended - */ - static Page *Concat(const Page &a, const Page &b); - - /** - * Increases the reference counter. - */ - void Ref() { - ref.Increment(); - } - - /** - * Decreases the reference counter. If it reaches zero, the #page is - * freed. - * - * @return true if the #page has been freed - */ - bool Unref(); -}; - -#endif diff --git a/src/output/Registry.cxx b/src/output/Registry.cxx index 6ce96fe57..42da59d15 100644 --- a/src/output/Registry.cxx +++ b/src/output/Registry.cxx @@ -23,7 +23,7 @@ #include "plugins/AlsaOutputPlugin.hxx" #include "plugins/AoOutputPlugin.hxx" #include "plugins/FifoOutputPlugin.hxx" -#include "plugins/HttpdOutputPlugin.hxx" +#include "plugins/httpd/HttpdOutputPlugin.hxx" #include "plugins/JackOutputPlugin.hxx" #include "plugins/NullOutputPlugin.hxx" #include "plugins/OpenALOutputPlugin.hxx" diff --git a/src/output/plugins/HttpdClient.cxx b/src/output/plugins/HttpdClient.cxx deleted file mode 100644 index d761bdf57..000000000 --- a/src/output/plugins/HttpdClient.cxx +++ /dev/null @@ -1,485 +0,0 @@ -/* - * Copyright (C) 2003-2014 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 "config.h" -#include "HttpdClient.hxx" -#include "HttpdInternal.hxx" -#include "util/ASCII.hxx" -#include "Page.hxx" -#include "IcyMetaDataServer.hxx" -#include "system/SocketError.hxx" -#include "Log.hxx" - -#include - -#include -#include - -HttpdClient::~HttpdClient() -{ - if (state == RESPONSE) { - if (current_page != nullptr) - current_page->Unref(); - - ClearQueue(); - } - - if (metadata) - metadata->Unref(); - - if (IsDefined()) - BufferedSocket::Close(); -} - -void -HttpdClient::Close() -{ - httpd.RemoveClient(*this); -} - -void -HttpdClient::LockClose() -{ - const ScopeLock protect(httpd.mutex); - Close(); -} - -void -HttpdClient::BeginResponse() -{ - assert(state != RESPONSE); - - state = RESPONSE; - current_page = nullptr; - - if (!head_method) - httpd.SendHeader(*this); -} - -/** - * Handle a line of the HTTP request. - */ -bool -HttpdClient::HandleLine(const char *line) -{ - assert(state != RESPONSE); - - if (state == REQUEST) { - if (memcmp(line, "HEAD /", 6) == 0) { - line += 6; - head_method = true; - } else if (memcmp(line, "GET /", 5) == 0) { - line += 5; - } else { - /* only GET is supported */ - LogWarning(httpd_output_domain, - "malformed request line from client"); - return false; - } - - line = strchr(line, ' '); - if (line == nullptr || memcmp(line + 1, "HTTP/", 5) != 0) { - /* HTTP/0.9 without request headers */ - - if (head_method) - return false; - - BeginResponse(); - return true; - } - - /* after the request line, request headers follow */ - state = HEADERS; - return true; - } else { - if (*line == 0) { - /* empty line: request is finished */ - - BeginResponse(); - return true; - } - - if (StringEqualsCaseASCII(line, "Icy-MetaData: 1", 15) || - StringEqualsCaseASCII(line, "Icy-MetaData:1", 14)) { - /* Send icy metadata */ - metadata_requested = metadata_supported; - return true; - } - - if (StringEqualsCaseASCII(line, "transferMode.dlna.org: Streaming", 32)) { - /* Send as dlna */ - dlna_streaming_requested = true; - /* metadata is not supported by dlna streaming, so disable it */ - metadata_supported = false; - metadata_requested = false; - return true; - } - - /* expect more request headers */ - return true; - } -} - -/** - * Sends the status line and response headers to the client. - */ -bool -HttpdClient::SendResponse() -{ - char buffer[1024]; - assert(state == RESPONSE); - - if (dlna_streaming_requested) { - snprintf(buffer, sizeof(buffer), - "HTTP/1.1 206 OK\r\n" - "Content-Type: %s\r\n" - "Content-Length: 10000\r\n" - "Content-RangeX: 0-1000000/1000000\r\n" - "transferMode.dlna.org: Streaming\r\n" - "Accept-Ranges: bytes\r\n" - "Connection: close\r\n" - "realTimeInfo.dlna.org: DLNA.ORG_TLAG=*\r\n" - "contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0\r\n" - "\r\n", - httpd.content_type); - - } else if (metadata_requested) { - char *metadata_header = - icy_server_metadata_header(httpd.name, httpd.genre, - httpd.website, - httpd.content_type, - metaint); - - g_strlcpy(buffer, metadata_header, sizeof(buffer)); - - delete[] metadata_header; - - } else { /* revert to a normal HTTP request */ - 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", - httpd.content_type); - } - - ssize_t nbytes = SocketMonitor::Write(buffer, strlen(buffer)); - if (gcc_unlikely(nbytes < 0)) { - const SocketErrorMessage msg; - FormatWarning(httpd_output_domain, - "failed to write to client: %s", - (const char *)msg); - Close(); - return false; - } - - return true; -} - -HttpdClient::HttpdClient(HttpdOutput &_httpd, int _fd, EventLoop &_loop, - bool _metadata_supported) - :BufferedSocket(_fd, _loop), - httpd(_httpd), - state(REQUEST), - queue_size(0), - head_method(false), - dlna_streaming_requested(false), - metadata_supported(_metadata_supported), - metadata_requested(false), metadata_sent(true), - metaint(8192), /*TODO: just a std value */ - metadata(nullptr), - metadata_current_position(0), metadata_fill(0) -{ -} - -void -HttpdClient::ClearQueue() -{ - assert(state == RESPONSE); - - while (!pages.empty()) { - Page *page = pages.front(); - pages.pop(); - -#ifndef NDEBUG - assert(queue_size >= page->size); - queue_size -= page->size; -#endif - - page->Unref(); - } - - assert(queue_size == 0); -} - -void -HttpdClient::CancelQueue() -{ - if (state != RESPONSE) - return; - - ClearQueue(); - - if (current_page == nullptr) - CancelWrite(); -} - -ssize_t -HttpdClient::TryWritePage(const Page &page, size_t position) -{ - assert(position < page.size); - - return Write(page.data + position, page.size - position); -} - -ssize_t -HttpdClient::TryWritePageN(const Page &page, size_t position, ssize_t n) -{ - return n >= 0 - ? Write(page.data + position, n) - : TryWritePage(page, position); -} - -ssize_t -HttpdClient::GetBytesTillMetaData() const -{ - if (metadata_requested && - current_page->size - current_position > metaint - metadata_fill) - return metaint - metadata_fill; - - return -1; -} - -inline bool -HttpdClient::TryWrite() -{ - const ScopeLock protect(httpd.mutex); - - assert(state == RESPONSE); - - if (current_page == nullptr) { - if (pages.empty()) { - /* another thread has removed the event source - while this thread was waiting for - httpd.mutex */ - CancelWrite(); - return true; - } - - current_page = pages.front(); - pages.pop(); - current_position = 0; - - assert(queue_size >= current_page->size); - queue_size -= current_page->size; - } - - const ssize_t bytes_to_write = GetBytesTillMetaData(); - if (bytes_to_write == 0) { - if (!metadata_sent) { - ssize_t nbytes = TryWritePage(*metadata, - metadata_current_position); - if (nbytes < 0) { - auto e = GetSocketError(); - if (IsSocketErrorAgain(e)) - return true; - - if (!IsSocketErrorClosed(e)) { - SocketErrorMessage msg(e); - FormatWarning(httpd_output_domain, - "failed to write to client: %s", - (const char *)msg); - } - - Close(); - return false; - } - - metadata_current_position += nbytes; - - if (metadata->size - metadata_current_position == 0) { - metadata_fill = 0; - metadata_current_position = 0; - metadata_sent = true; - } - } else { - guchar empty_data = 0; - - ssize_t nbytes = Write(&empty_data, 1); - if (nbytes < 0) { - auto e = GetSocketError(); - if (IsSocketErrorAgain(e)) - return true; - - if (!IsSocketErrorClosed(e)) { - SocketErrorMessage msg(e); - FormatWarning(httpd_output_domain, - "failed to write to client: %s", - (const char *)msg); - } - - Close(); - return false; - } - - metadata_fill = 0; - metadata_current_position = 0; - } - } else { - ssize_t nbytes = - TryWritePageN(*current_page, current_position, - bytes_to_write); - if (nbytes < 0) { - auto e = GetSocketError(); - if (IsSocketErrorAgain(e)) - return true; - - if (!IsSocketErrorClosed(e)) { - SocketErrorMessage msg(e); - FormatWarning(httpd_output_domain, - "failed to write to client: %s", - (const char *)msg); - } - - Close(); - return false; - } - - current_position += nbytes; - assert(current_position <= current_page->size); - - if (metadata_requested) - metadata_fill += nbytes; - - if (current_position >= current_page->size) { - current_page->Unref(); - current_page = nullptr; - - if (pages.empty()) - /* all pages are sent: remove the - event source */ - CancelWrite(); - } - } - - return true; -} - -void -HttpdClient::PushPage(Page *page) -{ - if (state != RESPONSE) - /* the client is still writing the HTTP request */ - return; - - if (queue_size > 256 * 1024) { - FormatDebug(httpd_output_domain, - "client is too slow, flushing its queue"); - ClearQueue(); - } - - page->Ref(); - pages.push(page); - queue_size += page->size; - - ScheduleWrite(); -} - -void -HttpdClient::PushMetaData(Page *page) -{ - if (metadata) { - metadata->Unref(); - metadata = nullptr; - } - - g_return_if_fail (page); - - page->Ref(); - metadata = page; - metadata_sent = false; -} - -bool -HttpdClient::OnSocketReady(unsigned flags) -{ - if (!BufferedSocket::OnSocketReady(flags)) - return false; - - if (flags & WRITE) - if (!TryWrite()) - return false; - - return true; -} - -BufferedSocket::InputResult -HttpdClient::OnSocketInput(void *data, size_t length) -{ - if (state == RESPONSE) { - LogWarning(httpd_output_domain, - "unexpected input from client"); - LockClose(); - return InputResult::CLOSED; - } - - char *line = (char *)data; - char *newline = (char *)memchr(line, '\n', length); - if (newline == nullptr) - return InputResult::MORE; - - ConsumeInput(newline + 1 - line); - - if (newline > line && newline[-1] == '\r') - --newline; - - /* terminate the string at the end of the line */ - *newline = 0; - - if (!HandleLine(line)) { - LockClose(); - return InputResult::CLOSED; - } - - if (state == RESPONSE) { - if (!SendResponse()) - return InputResult::CLOSED; - - if (head_method) { - LockClose(); - return InputResult::CLOSED; - } - } - - return InputResult::AGAIN; -} - -void -HttpdClient::OnSocketError(Error &&error) -{ - LogError(error); -} - -void -HttpdClient::OnSocketClosed() -{ - LockClose(); -} diff --git a/src/output/plugins/HttpdClient.hxx b/src/output/plugins/HttpdClient.hxx deleted file mode 100644 index f94f05769..000000000 --- a/src/output/plugins/HttpdClient.hxx +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright (C) 2003-2014 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. - */ - -#ifndef MPD_OUTPUT_HTTPD_CLIENT_HXX -#define MPD_OUTPUT_HTTPD_CLIENT_HXX - -#include "event/BufferedSocket.hxx" -#include "Compiler.h" - -#include -#include - -#include - -class HttpdOutput; -class Page; - -class HttpdClient final : BufferedSocket { - /** - * The httpd output object this client is connected to. - */ - HttpdOutput &httpd; - - /** - * 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. - */ - std::queue> pages; - - /** - * The sum of all page sizes in #pages. - */ - size_t queue_size; - - /** - * The #page which is currently being sent to the client. - */ - Page *current_page; - - /** - * The amount of bytes which were already sent from - * #current_page. - */ - size_t current_position; - - /** - * Is this a HEAD request? - */ - bool head_method; - - /** - * If DLNA streaming was an option. - */ - bool dlna_streaming_requested; - - /* ICY */ - - /** - * Do we support sending Icy-Metadata to the client? This is - * disabled if the httpd audio output uses encoder tags. - */ - bool metadata_supported; - - /** - * If we should sent icy metadata. - */ - bool metadata_requested; - - /** - * If the current metadata was already sent to the client. - */ - bool metadata_sent; - - /** - * The amount of streaming data between each metadata block - */ - unsigned metaint; - - /** - * The metadata as #Page which is currently being sent to the client. - */ - Page *metadata; - - /* - * The amount of bytes which were already sent from the metadata. - */ - size_t metadata_current_position; - - /** - * The amount of streaming data sent to the client - * since the last icy information was sent. - */ - unsigned metadata_fill; - -public: - /** - * @param httpd the HTTP output device - * @param fd the socket file descriptor - */ - HttpdClient(HttpdOutput &httpd, int _fd, EventLoop &_loop, - bool _metadata_supported); - - /** - * Note: this does not remove the client from the - * #HttpdOutput object. - */ - ~HttpdClient(); - - /** - * Frees the client and removes it from the server's client list. - */ - void Close(); - - void LockClose(); - - /** - * Clears the page queue. - */ - void CancelQueue(); - - /** - * Handle a line of the HTTP request. - */ - bool HandleLine(const char *line); - - /** - * Switch the client to the "RESPONSE" state. - */ - void BeginResponse(); - - /** - * Sends the status line and response headers to the client. - */ - bool SendResponse(); - - gcc_pure - ssize_t GetBytesTillMetaData() const; - - ssize_t TryWritePage(const Page &page, size_t position); - ssize_t TryWritePageN(const Page &page, size_t position, ssize_t n); - - bool TryWrite(); - - /** - * Appends a page to the client's queue. - */ - void PushPage(Page *page); - - /** - * Sends the passed metadata. - */ - void PushMetaData(Page *page); - -private: - void ClearQueue(); - -protected: - virtual bool OnSocketReady(unsigned flags) override; - virtual InputResult OnSocketInput(void *data, size_t length) override; - virtual void OnSocketError(Error &&error) override; - virtual void OnSocketClosed() override; -}; - -#endif diff --git a/src/output/plugins/HttpdInternal.hxx b/src/output/plugins/HttpdInternal.hxx deleted file mode 100644 index 53029edc6..000000000 --- a/src/output/plugins/HttpdInternal.hxx +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright (C) 2003-2014 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. - */ - -/** \file - * - * Internal declarations for the "httpd" audio output plugin. - */ - -#ifndef MPD_OUTPUT_HTTPD_INTERNAL_H -#define MPD_OUTPUT_HTTPD_INTERNAL_H - -#include "../Internal.hxx" -#include "../Timer.hxx" -#include "thread/Mutex.hxx" -#include "event/ServerSocket.hxx" -#include "event/DeferredMonitor.hxx" -#include "util/Cast.hxx" - -#ifdef _LIBCPP_VERSION -/* can't use incomplete template arguments with libc++ */ -#include "HttpdClient.hxx" -#endif - -#include -#include -#include - -struct config_param; -class Error; -class EventLoop; -class ServerSocket; -class HttpdClient; -class Page; -struct Encoder; -struct Tag; - -class HttpdOutput final : ServerSocket, DeferredMonitor { - AudioOutput base; - - /** - * True if the audio output is open and accepts client - * connections. - */ - bool open; - - /** - * The configured encoder plugin. - */ - Encoder *encoder; - - /** - * Number of bytes which were fed into the encoder, without - * ever receiving new output. This is used to estimate - * whether MPD should manually flush the encoder, to avoid - * buffer underruns in the client. - */ - size_t unflushed_input; - -public: - /** - * The MIME type produced by the #encoder. - */ - const char *content_type; - - /** - * This mutex protects the listener socket and the client - * list. - */ - mutable Mutex mutex; - - /** - * This condition gets signalled when an item is removed from - * #pages. - */ - Cond cond; - -private: - /** - * A #Timer object to synchronize this output with the - * wallclock. - */ - Timer *timer; - - /** - * The header page, which is sent to every client on connect. - */ - Page *header; - - /** - * The metadata, which is sent to every client. - */ - Page *metadata; - - /** - * The page queue, i.e. pages from the encoder to be - * broadcasted to all clients. This container is necessary to - * pass pages from the OutputThread to the IOThread. It is - * protected by #mutex, and removing signals #cond. - */ - std::queue> pages; - - public: - /** - * The configured name. - */ - char const *name; - /** - * The configured genre. - */ - char const *genre; - /** - * The configured website address. - */ - char const *website; - -private: - /** - * A linked list containing all clients which are currently - * connected. - */ - std::forward_list clients; - - /** - * A temporary buffer for the httpd_output_read_page() - * function. - */ - char buffer[32768]; - - /** - * The maximum and current number of clients connected - * at the same time. - */ - unsigned clients_max, clients_cnt; - -public: - HttpdOutput(EventLoop &_loop); - ~HttpdOutput(); - -#if GCC_CHECK_VERSION(4,6) || defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Winvalid-offsetof" -#endif - - static constexpr HttpdOutput *Cast(AudioOutput *ao) { - return ContainerCast(ao, HttpdOutput, base); - } - -#if GCC_CHECK_VERSION(4,6) || defined(__clang__) -#pragma GCC diagnostic pop -#endif - - using DeferredMonitor::GetEventLoop; - - bool Init(const config_param ¶m, Error &error); - - bool Configure(const config_param ¶m, Error &error); - - AudioOutput *InitAndConfigure(const config_param ¶m, - Error &error) { - if (!Init(param, error)) - return nullptr; - - if (!Configure(param, error)) - return nullptr; - - return &base; - } - - bool Bind(Error &error); - void Unbind(); - - /** - * Caller must lock the mutex. - */ - bool OpenEncoder(AudioFormat &audio_format, Error &error); - - /** - * Caller must lock the mutex. - */ - bool Open(AudioFormat &audio_format, Error &error); - - /** - * Caller must lock the mutex. - */ - void Close(); - - /** - * Check whether there is at least one client. - * - * Caller must lock the mutex. - */ - gcc_pure - bool HasClients() const { - return !clients.empty(); - } - - /** - * Check whether there is at least one client. - */ - gcc_pure - bool LockHasClients() const { - const ScopeLock protect(mutex); - return HasClients(); - } - - void AddClient(int fd); - - /** - * Removes a client from the httpd_output.clients linked list. - */ - void RemoveClient(HttpdClient &client); - - /** - * Sends the encoder header to the client. This is called - * right after the response headers have been sent. - */ - void SendHeader(HttpdClient &client) const; - - gcc_pure - unsigned Delay() const; - - /** - * Reads data from the encoder (as much as available) and - * returns it as a new #page object. - */ - Page *ReadPage(); - - /** - * Broadcasts a page struct to all clients. - * - * Mutext must not be locked. - */ - void BroadcastPage(Page *page); - - /** - * Broadcasts data from the encoder to all clients. - */ - void BroadcastFromEncoder(); - - bool EncodeAndPlay(const void *chunk, size_t size, Error &error); - - void SendTag(const Tag *tag); - - size_t Play(const void *chunk, size_t size, Error &error); - - void CancelAllClients(); - -private: - virtual void RunDeferred() override; - - virtual void OnAccept(int fd, const sockaddr &address, - size_t address_length, int uid) override; -}; - -extern const class Domain httpd_output_domain; - -#endif diff --git a/src/output/plugins/HttpdOutputPlugin.cxx b/src/output/plugins/HttpdOutputPlugin.cxx deleted file mode 100644 index 17a3df59a..000000000 --- a/src/output/plugins/HttpdOutputPlugin.cxx +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright (C) 2003-2014 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 "config.h" -#include "HttpdOutputPlugin.hxx" -#include "HttpdInternal.hxx" -#include "HttpdClient.hxx" -#include "../OutputAPI.hxx" -#include "encoder/EncoderPlugin.hxx" -#include "encoder/EncoderList.hxx" -#include "system/Resolver.hxx" -#include "Page.hxx" -#include "IcyMetaDataServer.hxx" -#include "system/fd_util.h" -#include "IOThread.hxx" -#include "event/Call.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include - -#include -#include -#include -#include - -#ifdef HAVE_LIBWRAP -#include /* needed for AF_UNIX */ -#include -#endif - -const Domain httpd_output_domain("httpd_output"); - -inline -HttpdOutput::HttpdOutput(EventLoop &_loop) - :ServerSocket(_loop), DeferredMonitor(_loop), - base(httpd_output_plugin), - encoder(nullptr), unflushed_input(0), - metadata(nullptr) -{ -} - -HttpdOutput::~HttpdOutput() -{ - if (metadata != nullptr) - metadata->Unref(); - - if (encoder != nullptr) - encoder_finish(encoder); - -} - -inline bool -HttpdOutput::Bind(Error &error) -{ - open = false; - - bool result = false; - BlockingCall(GetEventLoop(), [this, &error, &result](){ - result = ServerSocket::Open(error); - }); - return result; -} - -inline void -HttpdOutput::Unbind() -{ - assert(!open); - - BlockingCall(GetEventLoop(), [this](){ - ServerSocket::Close(); - }); -} - -inline bool -HttpdOutput::Configure(const config_param ¶m, Error &error) -{ - /* read configuration */ - name = param.GetBlockValue("name", "Set name in config"); - genre = param.GetBlockValue("genre", "Set genre in config"); - website = param.GetBlockValue("website", "Set website in config"); - - unsigned port = param.GetBlockValue("port", 8000u); - - const char *encoder_name = - param.GetBlockValue("encoder", "vorbis"); - const auto encoder_plugin = encoder_plugin_get(encoder_name); - if (encoder_plugin == nullptr) { - error.Format(httpd_output_domain, - "No such encoder: %s", encoder_name); - return false; - } - - clients_max = param.GetBlockValue("max_clients", 0u); - - /* set up bind_to_address */ - - const char *bind_to_address = param.GetBlockValue("bind_to_address"); - bool success = bind_to_address != nullptr && - strcmp(bind_to_address, "any") != 0 - ? AddHost(bind_to_address, port, error) - : AddPort(port, error); - if (!success) - return false; - - /* initialize encoder */ - - encoder = encoder_init(*encoder_plugin, param, error); - if (encoder == nullptr) - return false; - - /* determine content type */ - content_type = encoder_get_mime_type(encoder); - if (content_type == nullptr) - content_type = "application/octet-stream"; - - return true; -} - -inline bool -HttpdOutput::Init(const config_param ¶m, Error &error) -{ - return base.Configure(param, error); -} - -static AudioOutput * -httpd_output_init(const config_param ¶m, Error &error) -{ - HttpdOutput *httpd = new HttpdOutput(io_thread_get()); - - AudioOutput *result = httpd->InitAndConfigure(param, error); - if (result == nullptr) - delete httpd; - - return result; -} - -static void -httpd_output_finish(AudioOutput *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - delete httpd; -} - -/** - * Creates a new #HttpdClient object and adds it into the - * HttpdOutput.clients linked list. - */ -inline void -HttpdOutput::AddClient(int fd) -{ - clients.emplace_front(*this, fd, GetEventLoop(), - encoder->plugin.tag == nullptr); - ++clients_cnt; - - /* pass metadata to client */ - if (metadata != nullptr) - clients.front().PushMetaData(metadata); -} - -void -HttpdOutput::RunDeferred() -{ - /* this method runs in the IOThread; it broadcasts pages from - our own queue to all clients */ - - const ScopeLock protect(mutex); - - while (!pages.empty()) { - Page *page = pages.front(); - pages.pop(); - - for (auto &client : clients) - client.PushPage(page); - - page->Unref(); - } - - /* wake up the client that may be waiting for the queue to be - flushed */ - cond.broadcast(); -} - -void -HttpdOutput::OnAccept(int fd, const sockaddr &address, - size_t address_length, gcc_unused int uid) -{ - /* the listener socket has become readable - a client has - connected */ - -#ifdef HAVE_LIBWRAP - if (address.sa_family != AF_UNIX) { - const auto hostaddr = sockaddr_to_string(&address, - address_length); - // TODO: shall we obtain the program name from argv[0]? - const char *progname = "mpd"; - - struct request_info req; - request_init(&req, RQ_FILE, fd, RQ_DAEMON, progname, 0); - - fromhost(&req); - - if (!hosts_access(&req)) { - /* tcp wrappers says no */ - FormatWarning(httpd_output_domain, - "libwrap refused connection (libwrap=%s) from %s", - progname, hostaddr.c_str()); - close_socket(fd); - return; - } - } -#else - (void)address; - (void)address_length; -#endif /* HAVE_WRAP */ - - const ScopeLock protect(mutex); - - if (fd >= 0) { - /* can we allow additional client */ - if (open && (clients_max == 0 || clients_cnt < clients_max)) - AddClient(fd); - else - close_socket(fd); - } else if (fd < 0 && errno != EINTR) { - LogErrno(httpd_output_domain, "accept() failed"); - } -} - -Page * -HttpdOutput::ReadPage() -{ - if (unflushed_input >= 65536) { - /* we have fed a lot of input into the encoder, but it - didn't give anything back yet - flush now to avoid - buffer underruns */ - encoder_flush(encoder, IgnoreError()); - unflushed_input = 0; - } - - size_t size = 0; - do { - size_t nbytes = encoder_read(encoder, - buffer + size, - sizeof(buffer) - size); - if (nbytes == 0) - break; - - unflushed_input = 0; - - size += nbytes; - } while (size < sizeof(buffer)); - - if (size == 0) - return nullptr; - - return Page::Copy(buffer, size); -} - -static bool -httpd_output_enable(AudioOutput *ao, Error &error) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - return httpd->Bind(error); -} - -static void -httpd_output_disable(AudioOutput *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - httpd->Unbind(); -} - -inline bool -HttpdOutput::OpenEncoder(AudioFormat &audio_format, Error &error) -{ - if (!encoder_open(encoder, audio_format, error)) - return false; - - /* we have to remember the encoder header, i.e. the first - bytes of encoder output after opening it, because it has to - be sent to every new client */ - header = ReadPage(); - - unflushed_input = 0; - - return true; -} - -inline bool -HttpdOutput::Open(AudioFormat &audio_format, Error &error) -{ - assert(!open); - assert(clients.empty()); - - /* open the encoder */ - - if (!OpenEncoder(audio_format, error)) - return false; - - /* initialize other attributes */ - - clients_cnt = 0; - timer = new Timer(audio_format); - - open = true; - - return true; -} - -static bool -httpd_output_open(AudioOutput *ao, AudioFormat &audio_format, - Error &error) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - const ScopeLock protect(httpd->mutex); - return httpd->Open(audio_format, error); -} - -inline void -HttpdOutput::Close() -{ - assert(open); - - open = false; - - delete timer; - - BlockingCall(GetEventLoop(), [this](){ - clients.clear(); - }); - - if (header != nullptr) - header->Unref(); - - encoder_close(encoder); -} - -static void -httpd_output_close(AudioOutput *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - const ScopeLock protect(httpd->mutex); - httpd->Close(); -} - -void -HttpdOutput::RemoveClient(HttpdClient &client) -{ - assert(clients_cnt > 0); - - for (auto prev = clients.before_begin(), i = std::next(prev);; - prev = i, i = std::next(prev)) { - assert(i != clients.end()); - if (&*i == &client) { - clients.erase_after(prev); - clients_cnt--; - break; - } - } -} - -void -HttpdOutput::SendHeader(HttpdClient &client) const -{ - if (header != nullptr) - client.PushPage(header); -} - -inline unsigned -HttpdOutput::Delay() const -{ - if (!LockHasClients() && base.pause) { - /* if there's no client and this output is paused, - then httpd_output_pause() will not do anything, it - will not fill the buffer and it will not update the - timer; therefore, we reset the timer here */ - timer->Reset(); - - /* some arbitrary delay that is long enough to avoid - consuming too much CPU, and short enough to notice - new clients quickly enough */ - return 1000; - } - - return timer->IsStarted() - ? timer->GetDelay() - : 0; -} - -static unsigned -httpd_output_delay(AudioOutput *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - return httpd->Delay(); -} - -void -HttpdOutput::BroadcastPage(Page *page) -{ - assert(page != nullptr); - - mutex.lock(); - pages.push(page); - page->Ref(); - mutex.unlock(); - - DeferredMonitor::Schedule(); -} - -void -HttpdOutput::BroadcastFromEncoder() -{ - /* synchronize with the IOThread */ - mutex.lock(); - while (!pages.empty()) - cond.wait(mutex); - - Page *page; - while ((page = ReadPage()) != nullptr) - pages.push(page); - - mutex.unlock(); - - DeferredMonitor::Schedule(); -} - -inline bool -HttpdOutput::EncodeAndPlay(const void *chunk, size_t size, Error &error) -{ - if (!encoder_write(encoder, chunk, size, error)) - return false; - - unflushed_input += size; - - BroadcastFromEncoder(); - return true; -} - -inline size_t -HttpdOutput::Play(const void *chunk, size_t size, Error &error) -{ - if (LockHasClients()) { - if (!EncodeAndPlay(chunk, size, error)) - return 0; - } - - if (!timer->IsStarted()) - timer->Start(); - timer->Add(size); - - return size; -} - -static size_t -httpd_output_play(AudioOutput *ao, const void *chunk, size_t size, - Error &error) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - return httpd->Play(chunk, size, error); -} - -static bool -httpd_output_pause(AudioOutput *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - if (httpd->LockHasClients()) { - static const char silence[1020] = { 0 }; - return httpd_output_play(ao, silence, sizeof(silence), - IgnoreError()) > 0; - } else { - return true; - } -} - -inline void -HttpdOutput::SendTag(const Tag *tag) -{ - assert(tag != nullptr); - - if (encoder->plugin.tag != nullptr) { - /* embed encoder tags */ - - /* flush the current stream, and end it */ - - encoder_pre_tag(encoder, IgnoreError()); - BroadcastFromEncoder(); - - /* send the tag to the encoder - which starts a new - stream now */ - - encoder_tag(encoder, tag, IgnoreError()); - - /* the first page generated by the encoder will now be - used as the new "header" page, which is sent to all - new clients */ - - Page *page = ReadPage(); - if (page != nullptr) { - if (header != nullptr) - header->Unref(); - header = page; - BroadcastPage(page); - } - } else { - /* use Icy-Metadata */ - - if (metadata != nullptr) - metadata->Unref(); - - static constexpr TagType types[] = { - TAG_ALBUM, TAG_ARTIST, TAG_TITLE, - TAG_NUM_OF_ITEM_TYPES - }; - - metadata = icy_server_metadata_page(*tag, &types[0]); - if (metadata != nullptr) { - const ScopeLock protect(mutex); - for (auto &client : clients) - client.PushMetaData(metadata); - } - } -} - -static void -httpd_output_tag(AudioOutput *ao, const Tag *tag) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - httpd->SendTag(tag); -} - -inline void -HttpdOutput::CancelAllClients() -{ - const ScopeLock protect(mutex); - - while (!pages.empty()) { - Page *page = pages.front(); - pages.pop(); - page->Unref(); - } - - for (auto &client : clients) - client.CancelQueue(); - - cond.broadcast(); -} - -static void -httpd_output_cancel(AudioOutput *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - BlockingCall(io_thread_get(), [httpd](){ - httpd->CancelAllClients(); - }); -} - -const struct AudioOutputPlugin httpd_output_plugin = { - "httpd", - nullptr, - httpd_output_init, - httpd_output_finish, - httpd_output_enable, - httpd_output_disable, - httpd_output_open, - httpd_output_close, - httpd_output_delay, - httpd_output_tag, - httpd_output_play, - nullptr, - httpd_output_cancel, - httpd_output_pause, - nullptr, -}; diff --git a/src/output/plugins/HttpdOutputPlugin.hxx b/src/output/plugins/HttpdOutputPlugin.hxx deleted file mode 100644 index df99e2b43..000000000 --- a/src/output/plugins/HttpdOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 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. - */ - -#ifndef MPD_HTTPD_OUTPUT_PLUGIN_HXX -#define MPD_HTTPD_OUTPUT_PLUGIN_HXX - -extern const struct AudioOutputPlugin httpd_output_plugin; - -#endif diff --git a/src/output/plugins/httpd/HttpdClient.cxx b/src/output/plugins/httpd/HttpdClient.cxx new file mode 100644 index 000000000..d761bdf57 --- /dev/null +++ b/src/output/plugins/httpd/HttpdClient.cxx @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2003-2014 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 "config.h" +#include "HttpdClient.hxx" +#include "HttpdInternal.hxx" +#include "util/ASCII.hxx" +#include "Page.hxx" +#include "IcyMetaDataServer.hxx" +#include "system/SocketError.hxx" +#include "Log.hxx" + +#include + +#include +#include + +HttpdClient::~HttpdClient() +{ + if (state == RESPONSE) { + if (current_page != nullptr) + current_page->Unref(); + + ClearQueue(); + } + + if (metadata) + metadata->Unref(); + + if (IsDefined()) + BufferedSocket::Close(); +} + +void +HttpdClient::Close() +{ + httpd.RemoveClient(*this); +} + +void +HttpdClient::LockClose() +{ + const ScopeLock protect(httpd.mutex); + Close(); +} + +void +HttpdClient::BeginResponse() +{ + assert(state != RESPONSE); + + state = RESPONSE; + current_page = nullptr; + + if (!head_method) + httpd.SendHeader(*this); +} + +/** + * Handle a line of the HTTP request. + */ +bool +HttpdClient::HandleLine(const char *line) +{ + assert(state != RESPONSE); + + if (state == REQUEST) { + if (memcmp(line, "HEAD /", 6) == 0) { + line += 6; + head_method = true; + } else if (memcmp(line, "GET /", 5) == 0) { + line += 5; + } else { + /* only GET is supported */ + LogWarning(httpd_output_domain, + "malformed request line from client"); + return false; + } + + line = strchr(line, ' '); + if (line == nullptr || memcmp(line + 1, "HTTP/", 5) != 0) { + /* HTTP/0.9 without request headers */ + + if (head_method) + return false; + + BeginResponse(); + return true; + } + + /* after the request line, request headers follow */ + state = HEADERS; + return true; + } else { + if (*line == 0) { + /* empty line: request is finished */ + + BeginResponse(); + return true; + } + + if (StringEqualsCaseASCII(line, "Icy-MetaData: 1", 15) || + StringEqualsCaseASCII(line, "Icy-MetaData:1", 14)) { + /* Send icy metadata */ + metadata_requested = metadata_supported; + return true; + } + + if (StringEqualsCaseASCII(line, "transferMode.dlna.org: Streaming", 32)) { + /* Send as dlna */ + dlna_streaming_requested = true; + /* metadata is not supported by dlna streaming, so disable it */ + metadata_supported = false; + metadata_requested = false; + return true; + } + + /* expect more request headers */ + return true; + } +} + +/** + * Sends the status line and response headers to the client. + */ +bool +HttpdClient::SendResponse() +{ + char buffer[1024]; + assert(state == RESPONSE); + + if (dlna_streaming_requested) { + snprintf(buffer, sizeof(buffer), + "HTTP/1.1 206 OK\r\n" + "Content-Type: %s\r\n" + "Content-Length: 10000\r\n" + "Content-RangeX: 0-1000000/1000000\r\n" + "transferMode.dlna.org: Streaming\r\n" + "Accept-Ranges: bytes\r\n" + "Connection: close\r\n" + "realTimeInfo.dlna.org: DLNA.ORG_TLAG=*\r\n" + "contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0\r\n" + "\r\n", + httpd.content_type); + + } else if (metadata_requested) { + char *metadata_header = + icy_server_metadata_header(httpd.name, httpd.genre, + httpd.website, + httpd.content_type, + metaint); + + g_strlcpy(buffer, metadata_header, sizeof(buffer)); + + delete[] metadata_header; + + } else { /* revert to a normal HTTP request */ + 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", + httpd.content_type); + } + + ssize_t nbytes = SocketMonitor::Write(buffer, strlen(buffer)); + if (gcc_unlikely(nbytes < 0)) { + const SocketErrorMessage msg; + FormatWarning(httpd_output_domain, + "failed to write to client: %s", + (const char *)msg); + Close(); + return false; + } + + return true; +} + +HttpdClient::HttpdClient(HttpdOutput &_httpd, int _fd, EventLoop &_loop, + bool _metadata_supported) + :BufferedSocket(_fd, _loop), + httpd(_httpd), + state(REQUEST), + queue_size(0), + head_method(false), + dlna_streaming_requested(false), + metadata_supported(_metadata_supported), + metadata_requested(false), metadata_sent(true), + metaint(8192), /*TODO: just a std value */ + metadata(nullptr), + metadata_current_position(0), metadata_fill(0) +{ +} + +void +HttpdClient::ClearQueue() +{ + assert(state == RESPONSE); + + while (!pages.empty()) { + Page *page = pages.front(); + pages.pop(); + +#ifndef NDEBUG + assert(queue_size >= page->size); + queue_size -= page->size; +#endif + + page->Unref(); + } + + assert(queue_size == 0); +} + +void +HttpdClient::CancelQueue() +{ + if (state != RESPONSE) + return; + + ClearQueue(); + + if (current_page == nullptr) + CancelWrite(); +} + +ssize_t +HttpdClient::TryWritePage(const Page &page, size_t position) +{ + assert(position < page.size); + + return Write(page.data + position, page.size - position); +} + +ssize_t +HttpdClient::TryWritePageN(const Page &page, size_t position, ssize_t n) +{ + return n >= 0 + ? Write(page.data + position, n) + : TryWritePage(page, position); +} + +ssize_t +HttpdClient::GetBytesTillMetaData() const +{ + if (metadata_requested && + current_page->size - current_position > metaint - metadata_fill) + return metaint - metadata_fill; + + return -1; +} + +inline bool +HttpdClient::TryWrite() +{ + const ScopeLock protect(httpd.mutex); + + assert(state == RESPONSE); + + if (current_page == nullptr) { + if (pages.empty()) { + /* another thread has removed the event source + while this thread was waiting for + httpd.mutex */ + CancelWrite(); + return true; + } + + current_page = pages.front(); + pages.pop(); + current_position = 0; + + assert(queue_size >= current_page->size); + queue_size -= current_page->size; + } + + const ssize_t bytes_to_write = GetBytesTillMetaData(); + if (bytes_to_write == 0) { + if (!metadata_sent) { + ssize_t nbytes = TryWritePage(*metadata, + metadata_current_position); + if (nbytes < 0) { + auto e = GetSocketError(); + if (IsSocketErrorAgain(e)) + return true; + + if (!IsSocketErrorClosed(e)) { + SocketErrorMessage msg(e); + FormatWarning(httpd_output_domain, + "failed to write to client: %s", + (const char *)msg); + } + + Close(); + return false; + } + + metadata_current_position += nbytes; + + if (metadata->size - metadata_current_position == 0) { + metadata_fill = 0; + metadata_current_position = 0; + metadata_sent = true; + } + } else { + guchar empty_data = 0; + + ssize_t nbytes = Write(&empty_data, 1); + if (nbytes < 0) { + auto e = GetSocketError(); + if (IsSocketErrorAgain(e)) + return true; + + if (!IsSocketErrorClosed(e)) { + SocketErrorMessage msg(e); + FormatWarning(httpd_output_domain, + "failed to write to client: %s", + (const char *)msg); + } + + Close(); + return false; + } + + metadata_fill = 0; + metadata_current_position = 0; + } + } else { + ssize_t nbytes = + TryWritePageN(*current_page, current_position, + bytes_to_write); + if (nbytes < 0) { + auto e = GetSocketError(); + if (IsSocketErrorAgain(e)) + return true; + + if (!IsSocketErrorClosed(e)) { + SocketErrorMessage msg(e); + FormatWarning(httpd_output_domain, + "failed to write to client: %s", + (const char *)msg); + } + + Close(); + return false; + } + + current_position += nbytes; + assert(current_position <= current_page->size); + + if (metadata_requested) + metadata_fill += nbytes; + + if (current_position >= current_page->size) { + current_page->Unref(); + current_page = nullptr; + + if (pages.empty()) + /* all pages are sent: remove the + event source */ + CancelWrite(); + } + } + + return true; +} + +void +HttpdClient::PushPage(Page *page) +{ + if (state != RESPONSE) + /* the client is still writing the HTTP request */ + return; + + if (queue_size > 256 * 1024) { + FormatDebug(httpd_output_domain, + "client is too slow, flushing its queue"); + ClearQueue(); + } + + page->Ref(); + pages.push(page); + queue_size += page->size; + + ScheduleWrite(); +} + +void +HttpdClient::PushMetaData(Page *page) +{ + if (metadata) { + metadata->Unref(); + metadata = nullptr; + } + + g_return_if_fail (page); + + page->Ref(); + metadata = page; + metadata_sent = false; +} + +bool +HttpdClient::OnSocketReady(unsigned flags) +{ + if (!BufferedSocket::OnSocketReady(flags)) + return false; + + if (flags & WRITE) + if (!TryWrite()) + return false; + + return true; +} + +BufferedSocket::InputResult +HttpdClient::OnSocketInput(void *data, size_t length) +{ + if (state == RESPONSE) { + LogWarning(httpd_output_domain, + "unexpected input from client"); + LockClose(); + return InputResult::CLOSED; + } + + char *line = (char *)data; + char *newline = (char *)memchr(line, '\n', length); + if (newline == nullptr) + return InputResult::MORE; + + ConsumeInput(newline + 1 - line); + + if (newline > line && newline[-1] == '\r') + --newline; + + /* terminate the string at the end of the line */ + *newline = 0; + + if (!HandleLine(line)) { + LockClose(); + return InputResult::CLOSED; + } + + if (state == RESPONSE) { + if (!SendResponse()) + return InputResult::CLOSED; + + if (head_method) { + LockClose(); + return InputResult::CLOSED; + } + } + + return InputResult::AGAIN; +} + +void +HttpdClient::OnSocketError(Error &&error) +{ + LogError(error); +} + +void +HttpdClient::OnSocketClosed() +{ + LockClose(); +} diff --git a/src/output/plugins/httpd/HttpdClient.hxx b/src/output/plugins/httpd/HttpdClient.hxx new file mode 100644 index 000000000..f94f05769 --- /dev/null +++ b/src/output/plugins/httpd/HttpdClient.hxx @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2003-2014 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. + */ + +#ifndef MPD_OUTPUT_HTTPD_CLIENT_HXX +#define MPD_OUTPUT_HTTPD_CLIENT_HXX + +#include "event/BufferedSocket.hxx" +#include "Compiler.h" + +#include +#include + +#include + +class HttpdOutput; +class Page; + +class HttpdClient final : BufferedSocket { + /** + * The httpd output object this client is connected to. + */ + HttpdOutput &httpd; + + /** + * 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. + */ + std::queue> pages; + + /** + * The sum of all page sizes in #pages. + */ + size_t queue_size; + + /** + * The #page which is currently being sent to the client. + */ + Page *current_page; + + /** + * The amount of bytes which were already sent from + * #current_page. + */ + size_t current_position; + + /** + * Is this a HEAD request? + */ + bool head_method; + + /** + * If DLNA streaming was an option. + */ + bool dlna_streaming_requested; + + /* ICY */ + + /** + * Do we support sending Icy-Metadata to the client? This is + * disabled if the httpd audio output uses encoder tags. + */ + bool metadata_supported; + + /** + * If we should sent icy metadata. + */ + bool metadata_requested; + + /** + * If the current metadata was already sent to the client. + */ + bool metadata_sent; + + /** + * The amount of streaming data between each metadata block + */ + unsigned metaint; + + /** + * The metadata as #Page which is currently being sent to the client. + */ + Page *metadata; + + /* + * The amount of bytes which were already sent from the metadata. + */ + size_t metadata_current_position; + + /** + * The amount of streaming data sent to the client + * since the last icy information was sent. + */ + unsigned metadata_fill; + +public: + /** + * @param httpd the HTTP output device + * @param fd the socket file descriptor + */ + HttpdClient(HttpdOutput &httpd, int _fd, EventLoop &_loop, + bool _metadata_supported); + + /** + * Note: this does not remove the client from the + * #HttpdOutput object. + */ + ~HttpdClient(); + + /** + * Frees the client and removes it from the server's client list. + */ + void Close(); + + void LockClose(); + + /** + * Clears the page queue. + */ + void CancelQueue(); + + /** + * Handle a line of the HTTP request. + */ + bool HandleLine(const char *line); + + /** + * Switch the client to the "RESPONSE" state. + */ + void BeginResponse(); + + /** + * Sends the status line and response headers to the client. + */ + bool SendResponse(); + + gcc_pure + ssize_t GetBytesTillMetaData() const; + + ssize_t TryWritePage(const Page &page, size_t position); + ssize_t TryWritePageN(const Page &page, size_t position, ssize_t n); + + bool TryWrite(); + + /** + * Appends a page to the client's queue. + */ + void PushPage(Page *page); + + /** + * Sends the passed metadata. + */ + void PushMetaData(Page *page); + +private: + void ClearQueue(); + +protected: + virtual bool OnSocketReady(unsigned flags) override; + virtual InputResult OnSocketInput(void *data, size_t length) override; + virtual void OnSocketError(Error &&error) override; + virtual void OnSocketClosed() override; +}; + +#endif diff --git a/src/output/plugins/httpd/HttpdInternal.hxx b/src/output/plugins/httpd/HttpdInternal.hxx new file mode 100644 index 000000000..a16c60bc3 --- /dev/null +++ b/src/output/plugins/httpd/HttpdInternal.hxx @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2003-2014 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. + */ + +/** \file + * + * Internal declarations for the "httpd" audio output plugin. + */ + +#ifndef MPD_OUTPUT_HTTPD_INTERNAL_H +#define MPD_OUTPUT_HTTPD_INTERNAL_H + +#include "output/Internal.hxx" +#include "output/Timer.hxx" +#include "thread/Mutex.hxx" +#include "event/ServerSocket.hxx" +#include "event/DeferredMonitor.hxx" +#include "util/Cast.hxx" + +#ifdef _LIBCPP_VERSION +/* can't use incomplete template arguments with libc++ */ +#include "HttpdClient.hxx" +#endif + +#include +#include +#include + +struct config_param; +class Error; +class EventLoop; +class ServerSocket; +class HttpdClient; +class Page; +struct Encoder; +struct Tag; + +class HttpdOutput final : ServerSocket, DeferredMonitor { + AudioOutput base; + + /** + * True if the audio output is open and accepts client + * connections. + */ + bool open; + + /** + * The configured encoder plugin. + */ + Encoder *encoder; + + /** + * Number of bytes which were fed into the encoder, without + * ever receiving new output. This is used to estimate + * whether MPD should manually flush the encoder, to avoid + * buffer underruns in the client. + */ + size_t unflushed_input; + +public: + /** + * The MIME type produced by the #encoder. + */ + const char *content_type; + + /** + * This mutex protects the listener socket and the client + * list. + */ + mutable Mutex mutex; + + /** + * This condition gets signalled when an item is removed from + * #pages. + */ + Cond cond; + +private: + /** + * A #Timer object to synchronize this output with the + * wallclock. + */ + Timer *timer; + + /** + * The header page, which is sent to every client on connect. + */ + Page *header; + + /** + * The metadata, which is sent to every client. + */ + Page *metadata; + + /** + * The page queue, i.e. pages from the encoder to be + * broadcasted to all clients. This container is necessary to + * pass pages from the OutputThread to the IOThread. It is + * protected by #mutex, and removing signals #cond. + */ + std::queue> pages; + + public: + /** + * The configured name. + */ + char const *name; + /** + * The configured genre. + */ + char const *genre; + /** + * The configured website address. + */ + char const *website; + +private: + /** + * A linked list containing all clients which are currently + * connected. + */ + std::forward_list clients; + + /** + * A temporary buffer for the httpd_output_read_page() + * function. + */ + char buffer[32768]; + + /** + * The maximum and current number of clients connected + * at the same time. + */ + unsigned clients_max, clients_cnt; + +public: + HttpdOutput(EventLoop &_loop); + ~HttpdOutput(); + +#if GCC_CHECK_VERSION(4,6) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#endif + + static constexpr HttpdOutput *Cast(AudioOutput *ao) { + return ContainerCast(ao, HttpdOutput, base); + } + +#if GCC_CHECK_VERSION(4,6) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + + using DeferredMonitor::GetEventLoop; + + bool Init(const config_param ¶m, Error &error); + + bool Configure(const config_param ¶m, Error &error); + + AudioOutput *InitAndConfigure(const config_param ¶m, + Error &error) { + if (!Init(param, error)) + return nullptr; + + if (!Configure(param, error)) + return nullptr; + + return &base; + } + + bool Bind(Error &error); + void Unbind(); + + /** + * Caller must lock the mutex. + */ + bool OpenEncoder(AudioFormat &audio_format, Error &error); + + /** + * Caller must lock the mutex. + */ + bool Open(AudioFormat &audio_format, Error &error); + + /** + * Caller must lock the mutex. + */ + void Close(); + + /** + * Check whether there is at least one client. + * + * Caller must lock the mutex. + */ + gcc_pure + bool HasClients() const { + return !clients.empty(); + } + + /** + * Check whether there is at least one client. + */ + gcc_pure + bool LockHasClients() const { + const ScopeLock protect(mutex); + return HasClients(); + } + + void AddClient(int fd); + + /** + * Removes a client from the httpd_output.clients linked list. + */ + void RemoveClient(HttpdClient &client); + + /** + * Sends the encoder header to the client. This is called + * right after the response headers have been sent. + */ + void SendHeader(HttpdClient &client) const; + + gcc_pure + unsigned Delay() const; + + /** + * Reads data from the encoder (as much as available) and + * returns it as a new #page object. + */ + Page *ReadPage(); + + /** + * Broadcasts a page struct to all clients. + * + * Mutext must not be locked. + */ + void BroadcastPage(Page *page); + + /** + * Broadcasts data from the encoder to all clients. + */ + void BroadcastFromEncoder(); + + bool EncodeAndPlay(const void *chunk, size_t size, Error &error); + + void SendTag(const Tag *tag); + + size_t Play(const void *chunk, size_t size, Error &error); + + void CancelAllClients(); + +private: + virtual void RunDeferred() override; + + virtual void OnAccept(int fd, const sockaddr &address, + size_t address_length, int uid) override; +}; + +extern const class Domain httpd_output_domain; + +#endif diff --git a/src/output/plugins/httpd/HttpdOutputPlugin.cxx b/src/output/plugins/httpd/HttpdOutputPlugin.cxx new file mode 100644 index 000000000..e3ba7727d --- /dev/null +++ b/src/output/plugins/httpd/HttpdOutputPlugin.cxx @@ -0,0 +1,601 @@ +/* + * Copyright (C) 2003-2014 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 "config.h" +#include "HttpdOutputPlugin.hxx" +#include "HttpdInternal.hxx" +#include "HttpdClient.hxx" +#include "output/OutputAPI.hxx" +#include "encoder/EncoderPlugin.hxx" +#include "encoder/EncoderList.hxx" +#include "system/Resolver.hxx" +#include "Page.hxx" +#include "IcyMetaDataServer.hxx" +#include "system/fd_util.h" +#include "IOThread.hxx" +#include "event/Call.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +#include +#include +#include +#include + +#ifdef HAVE_LIBWRAP +#include /* needed for AF_UNIX */ +#include +#endif + +const Domain httpd_output_domain("httpd_output"); + +inline +HttpdOutput::HttpdOutput(EventLoop &_loop) + :ServerSocket(_loop), DeferredMonitor(_loop), + base(httpd_output_plugin), + encoder(nullptr), unflushed_input(0), + metadata(nullptr) +{ +} + +HttpdOutput::~HttpdOutput() +{ + if (metadata != nullptr) + metadata->Unref(); + + if (encoder != nullptr) + encoder_finish(encoder); + +} + +inline bool +HttpdOutput::Bind(Error &error) +{ + open = false; + + bool result = false; + BlockingCall(GetEventLoop(), [this, &error, &result](){ + result = ServerSocket::Open(error); + }); + return result; +} + +inline void +HttpdOutput::Unbind() +{ + assert(!open); + + BlockingCall(GetEventLoop(), [this](){ + ServerSocket::Close(); + }); +} + +inline bool +HttpdOutput::Configure(const config_param ¶m, Error &error) +{ + /* read configuration */ + name = param.GetBlockValue("name", "Set name in config"); + genre = param.GetBlockValue("genre", "Set genre in config"); + website = param.GetBlockValue("website", "Set website in config"); + + unsigned port = param.GetBlockValue("port", 8000u); + + const char *encoder_name = + param.GetBlockValue("encoder", "vorbis"); + const auto encoder_plugin = encoder_plugin_get(encoder_name); + if (encoder_plugin == nullptr) { + error.Format(httpd_output_domain, + "No such encoder: %s", encoder_name); + return false; + } + + clients_max = param.GetBlockValue("max_clients", 0u); + + /* set up bind_to_address */ + + const char *bind_to_address = param.GetBlockValue("bind_to_address"); + bool success = bind_to_address != nullptr && + strcmp(bind_to_address, "any") != 0 + ? AddHost(bind_to_address, port, error) + : AddPort(port, error); + if (!success) + return false; + + /* initialize encoder */ + + encoder = encoder_init(*encoder_plugin, param, error); + if (encoder == nullptr) + return false; + + /* determine content type */ + content_type = encoder_get_mime_type(encoder); + if (content_type == nullptr) + content_type = "application/octet-stream"; + + return true; +} + +inline bool +HttpdOutput::Init(const config_param ¶m, Error &error) +{ + return base.Configure(param, error); +} + +static AudioOutput * +httpd_output_init(const config_param ¶m, Error &error) +{ + HttpdOutput *httpd = new HttpdOutput(io_thread_get()); + + AudioOutput *result = httpd->InitAndConfigure(param, error); + if (result == nullptr) + delete httpd; + + return result; +} + +static void +httpd_output_finish(AudioOutput *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + delete httpd; +} + +/** + * Creates a new #HttpdClient object and adds it into the + * HttpdOutput.clients linked list. + */ +inline void +HttpdOutput::AddClient(int fd) +{ + clients.emplace_front(*this, fd, GetEventLoop(), + encoder->plugin.tag == nullptr); + ++clients_cnt; + + /* pass metadata to client */ + if (metadata != nullptr) + clients.front().PushMetaData(metadata); +} + +void +HttpdOutput::RunDeferred() +{ + /* this method runs in the IOThread; it broadcasts pages from + our own queue to all clients */ + + const ScopeLock protect(mutex); + + while (!pages.empty()) { + Page *page = pages.front(); + pages.pop(); + + for (auto &client : clients) + client.PushPage(page); + + page->Unref(); + } + + /* wake up the client that may be waiting for the queue to be + flushed */ + cond.broadcast(); +} + +void +HttpdOutput::OnAccept(int fd, const sockaddr &address, + size_t address_length, gcc_unused int uid) +{ + /* the listener socket has become readable - a client has + connected */ + +#ifdef HAVE_LIBWRAP + if (address.sa_family != AF_UNIX) { + const auto hostaddr = sockaddr_to_string(&address, + address_length); + // TODO: shall we obtain the program name from argv[0]? + const char *progname = "mpd"; + + struct request_info req; + request_init(&req, RQ_FILE, fd, RQ_DAEMON, progname, 0); + + fromhost(&req); + + if (!hosts_access(&req)) { + /* tcp wrappers says no */ + FormatWarning(httpd_output_domain, + "libwrap refused connection (libwrap=%s) from %s", + progname, hostaddr.c_str()); + close_socket(fd); + return; + } + } +#else + (void)address; + (void)address_length; +#endif /* HAVE_WRAP */ + + const ScopeLock protect(mutex); + + if (fd >= 0) { + /* can we allow additional client */ + if (open && (clients_max == 0 || clients_cnt < clients_max)) + AddClient(fd); + else + close_socket(fd); + } else if (fd < 0 && errno != EINTR) { + LogErrno(httpd_output_domain, "accept() failed"); + } +} + +Page * +HttpdOutput::ReadPage() +{ + if (unflushed_input >= 65536) { + /* we have fed a lot of input into the encoder, but it + didn't give anything back yet - flush now to avoid + buffer underruns */ + encoder_flush(encoder, IgnoreError()); + unflushed_input = 0; + } + + size_t size = 0; + do { + size_t nbytes = encoder_read(encoder, + buffer + size, + sizeof(buffer) - size); + if (nbytes == 0) + break; + + unflushed_input = 0; + + size += nbytes; + } while (size < sizeof(buffer)); + + if (size == 0) + return nullptr; + + return Page::Copy(buffer, size); +} + +static bool +httpd_output_enable(AudioOutput *ao, Error &error) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + return httpd->Bind(error); +} + +static void +httpd_output_disable(AudioOutput *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + httpd->Unbind(); +} + +inline bool +HttpdOutput::OpenEncoder(AudioFormat &audio_format, Error &error) +{ + if (!encoder_open(encoder, audio_format, error)) + return false; + + /* we have to remember the encoder header, i.e. the first + bytes of encoder output after opening it, because it has to + be sent to every new client */ + header = ReadPage(); + + unflushed_input = 0; + + return true; +} + +inline bool +HttpdOutput::Open(AudioFormat &audio_format, Error &error) +{ + assert(!open); + assert(clients.empty()); + + /* open the encoder */ + + if (!OpenEncoder(audio_format, error)) + return false; + + /* initialize other attributes */ + + clients_cnt = 0; + timer = new Timer(audio_format); + + open = true; + + return true; +} + +static bool +httpd_output_open(AudioOutput *ao, AudioFormat &audio_format, + Error &error) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + const ScopeLock protect(httpd->mutex); + return httpd->Open(audio_format, error); +} + +inline void +HttpdOutput::Close() +{ + assert(open); + + open = false; + + delete timer; + + BlockingCall(GetEventLoop(), [this](){ + clients.clear(); + }); + + if (header != nullptr) + header->Unref(); + + encoder_close(encoder); +} + +static void +httpd_output_close(AudioOutput *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + const ScopeLock protect(httpd->mutex); + httpd->Close(); +} + +void +HttpdOutput::RemoveClient(HttpdClient &client) +{ + assert(clients_cnt > 0); + + for (auto prev = clients.before_begin(), i = std::next(prev);; + prev = i, i = std::next(prev)) { + assert(i != clients.end()); + if (&*i == &client) { + clients.erase_after(prev); + clients_cnt--; + break; + } + } +} + +void +HttpdOutput::SendHeader(HttpdClient &client) const +{ + if (header != nullptr) + client.PushPage(header); +} + +inline unsigned +HttpdOutput::Delay() const +{ + if (!LockHasClients() && base.pause) { + /* if there's no client and this output is paused, + then httpd_output_pause() will not do anything, it + will not fill the buffer and it will not update the + timer; therefore, we reset the timer here */ + timer->Reset(); + + /* some arbitrary delay that is long enough to avoid + consuming too much CPU, and short enough to notice + new clients quickly enough */ + return 1000; + } + + return timer->IsStarted() + ? timer->GetDelay() + : 0; +} + +static unsigned +httpd_output_delay(AudioOutput *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + return httpd->Delay(); +} + +void +HttpdOutput::BroadcastPage(Page *page) +{ + assert(page != nullptr); + + mutex.lock(); + pages.push(page); + page->Ref(); + mutex.unlock(); + + DeferredMonitor::Schedule(); +} + +void +HttpdOutput::BroadcastFromEncoder() +{ + /* synchronize with the IOThread */ + mutex.lock(); + while (!pages.empty()) + cond.wait(mutex); + + Page *page; + while ((page = ReadPage()) != nullptr) + pages.push(page); + + mutex.unlock(); + + DeferredMonitor::Schedule(); +} + +inline bool +HttpdOutput::EncodeAndPlay(const void *chunk, size_t size, Error &error) +{ + if (!encoder_write(encoder, chunk, size, error)) + return false; + + unflushed_input += size; + + BroadcastFromEncoder(); + return true; +} + +inline size_t +HttpdOutput::Play(const void *chunk, size_t size, Error &error) +{ + if (LockHasClients()) { + if (!EncodeAndPlay(chunk, size, error)) + return 0; + } + + if (!timer->IsStarted()) + timer->Start(); + timer->Add(size); + + return size; +} + +static size_t +httpd_output_play(AudioOutput *ao, const void *chunk, size_t size, + Error &error) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + return httpd->Play(chunk, size, error); +} + +static bool +httpd_output_pause(AudioOutput *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + if (httpd->LockHasClients()) { + static const char silence[1020] = { 0 }; + return httpd_output_play(ao, silence, sizeof(silence), + IgnoreError()) > 0; + } else { + return true; + } +} + +inline void +HttpdOutput::SendTag(const Tag *tag) +{ + assert(tag != nullptr); + + if (encoder->plugin.tag != nullptr) { + /* embed encoder tags */ + + /* flush the current stream, and end it */ + + encoder_pre_tag(encoder, IgnoreError()); + BroadcastFromEncoder(); + + /* send the tag to the encoder - which starts a new + stream now */ + + encoder_tag(encoder, tag, IgnoreError()); + + /* the first page generated by the encoder will now be + used as the new "header" page, which is sent to all + new clients */ + + Page *page = ReadPage(); + if (page != nullptr) { + if (header != nullptr) + header->Unref(); + header = page; + BroadcastPage(page); + } + } else { + /* use Icy-Metadata */ + + if (metadata != nullptr) + metadata->Unref(); + + static constexpr TagType types[] = { + TAG_ALBUM, TAG_ARTIST, TAG_TITLE, + TAG_NUM_OF_ITEM_TYPES + }; + + metadata = icy_server_metadata_page(*tag, &types[0]); + if (metadata != nullptr) { + const ScopeLock protect(mutex); + for (auto &client : clients) + client.PushMetaData(metadata); + } + } +} + +static void +httpd_output_tag(AudioOutput *ao, const Tag *tag) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + httpd->SendTag(tag); +} + +inline void +HttpdOutput::CancelAllClients() +{ + const ScopeLock protect(mutex); + + while (!pages.empty()) { + Page *page = pages.front(); + pages.pop(); + page->Unref(); + } + + for (auto &client : clients) + client.CancelQueue(); + + cond.broadcast(); +} + +static void +httpd_output_cancel(AudioOutput *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + BlockingCall(io_thread_get(), [httpd](){ + httpd->CancelAllClients(); + }); +} + +const struct AudioOutputPlugin httpd_output_plugin = { + "httpd", + nullptr, + httpd_output_init, + httpd_output_finish, + httpd_output_enable, + httpd_output_disable, + httpd_output_open, + httpd_output_close, + httpd_output_delay, + httpd_output_tag, + httpd_output_play, + nullptr, + httpd_output_cancel, + httpd_output_pause, + nullptr, +}; diff --git a/src/output/plugins/httpd/HttpdOutputPlugin.hxx b/src/output/plugins/httpd/HttpdOutputPlugin.hxx new file mode 100644 index 000000000..df99e2b43 --- /dev/null +++ b/src/output/plugins/httpd/HttpdOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 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. + */ + +#ifndef MPD_HTTPD_OUTPUT_PLUGIN_HXX +#define MPD_HTTPD_OUTPUT_PLUGIN_HXX + +extern const struct AudioOutputPlugin httpd_output_plugin; + +#endif diff --git a/src/output/plugins/httpd/IcyMetaDataServer.cxx b/src/output/plugins/httpd/IcyMetaDataServer.cxx new file mode 100644 index 000000000..146df23d1 --- /dev/null +++ b/src/output/plugins/httpd/IcyMetaDataServer.cxx @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2003-2014 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 "config.h" +#include "IcyMetaDataServer.hxx" +#include "Page.hxx" +#include "tag/Tag.hxx" +#include "util/FormatString.hxx" + +#include + +#include + +char* +icy_server_metadata_header(const char *name, + const char *genre, const char *url, + const char *content_type, int metaint) +{ + return FormatNew("ICY 200 OK\r\n" + "icy-notice1:
This stream requires an audio player!
\r\n" /* TODO */ + "icy-notice2:MPD - The music player daemon
\r\n" + "icy-name: %s\r\n" /* TODO */ + "icy-genre: %s\r\n" /* TODO */ + "icy-url: %s\r\n" /* TODO */ + "icy-pub:1\r\n" + "icy-metaint:%d\r\n" + /* TODO "icy-br:%d\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", + name, + genre, + url, + metaint, + /* bitrate, */ + content_type); +} + +static char * +icy_server_metadata_string(const char *stream_title, const char* stream_url) +{ + gchar *icy_metadata; + guint meta_length; + + // The leading n is a placeholder for the length information + icy_metadata = FormatNew("nStreamTitle='%s';" + "StreamUrl='%s';", + stream_title, + stream_url); + + meta_length = strlen(icy_metadata); + + meta_length--; // subtract placeholder + + meta_length = ((int)meta_length / 16) + 1; + + icy_metadata[0] = meta_length; + + if (meta_length > 255) { + delete[] icy_metadata; + return nullptr; + } + + return icy_metadata; +} + +Page * +icy_server_metadata_page(const Tag &tag, const TagType *types) +{ + const gchar *tag_items[TAG_NUM_OF_ITEM_TYPES]; + gint last_item, item; + guint position; + gchar *icy_string; + gchar stream_title[(1 + 255 - 28) * 16]; // Length + Metadata - + // "StreamTitle='';StreamUrl='';" + // = 4081 - 28 + stream_title[0] = '\0'; + + last_item = -1; + + while (*types != TAG_NUM_OF_ITEM_TYPES) { + const gchar *tag_item = tag.GetValue(*types++); + if (tag_item) + tag_items[++last_item] = tag_item; + } + + position = item = 0; + while (position < sizeof(stream_title) && item <= last_item) { + gint length = 0; + + length = g_strlcpy(stream_title + position, + tag_items[item++], + sizeof(stream_title) - position); + + position += length; + + if (item <= last_item) { + length = g_strlcpy(stream_title + position, + " - ", + sizeof(stream_title) - position); + + position += length; + } + } + + icy_string = icy_server_metadata_string(stream_title, ""); + + if (icy_string == nullptr) + return nullptr; + + Page *icy_metadata = Page::Copy(icy_string, (icy_string[0] * 16) + 1); + + delete[] icy_string; + + return icy_metadata; +} diff --git a/src/output/plugins/httpd/IcyMetaDataServer.hxx b/src/output/plugins/httpd/IcyMetaDataServer.hxx new file mode 100644 index 000000000..773b46641 --- /dev/null +++ b/src/output/plugins/httpd/IcyMetaDataServer.hxx @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2003-2014 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. + */ + +#ifndef MPD_ICY_META_DATA_SERVER_HXX +#define MPD_ICY_META_DATA_SERVER_HXX + +#include "tag/TagType.h" + +struct Tag; +class Page; + +/** + * Free the return value with delete[]. + */ +char* +icy_server_metadata_header(const char *name, + const char *genre, const char *url, + const char *content_type, int metaint); + +Page * +icy_server_metadata_page(const Tag &tag, const TagType *types); + +#endif diff --git a/src/output/plugins/httpd/Page.cxx b/src/output/plugins/httpd/Page.cxx new file mode 100644 index 000000000..e22134bbc --- /dev/null +++ b/src/output/plugins/httpd/Page.cxx @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2003-2014 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 "config.h" +#include "Page.hxx" +#include "util/Alloc.hxx" + +#include + +#include +#include +#include + +Page * +Page::Create(size_t size) +{ + void *p = xalloc(sizeof(Page) + size - + sizeof(Page::data)); + return ::new(p) Page(size); +} + +Page * +Page::Copy(const void *data, size_t size) +{ + assert(data != nullptr); + + Page *page = Create(size); + memcpy(page->data, data, size); + return page; +} + +Page * +Page::Concat(const Page &a, const Page &b) +{ + Page *page = Create(a.size + b.size); + + memcpy(page->data, a.data, a.size); + memcpy(page->data + a.size, b.data, b.size); + + return page; +} + +bool +Page::Unref() +{ + bool unused = ref.Decrement(); + + if (unused) { + this->Page::~Page(); + free(this); + } + + return unused; +} diff --git a/src/output/plugins/httpd/Page.hxx b/src/output/plugins/httpd/Page.hxx new file mode 100644 index 000000000..95f35d06a --- /dev/null +++ b/src/output/plugins/httpd/Page.hxx @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2003-2014 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. + */ + +/** \file + * + * This is a library which manages reference counted buffers. + */ + +#ifndef MPD_PAGE_HXX +#define MPD_PAGE_HXX + +#include "util/RefCount.hxx" + +#include + +/** + * A dynamically allocated buffer which keeps track of its reference + * count. This is useful for passing buffers around, when several + * instances hold references to one buffer. + */ +class Page { + /** + * The number of references to this buffer. This library uses + * atomic functions to access it, i.e. no locks are required. + * As soon as this attribute reaches zero, the buffer is + * freed. + */ + RefCount ref; + +public: + /** + * The size of this buffer in bytes. + */ + const size_t size; + + /** + * Dynamic array containing the buffer data. + */ + unsigned char data[sizeof(long)]; + +protected: + Page(size_t _size):size(_size) {} + ~Page() = default; + + /** + * Allocates a new #Page object, without filling the data + * element. + */ + static Page *Create(size_t size); + +public: + /** + * Creates a new #page object, and copies data from the + * specified buffer. It is initialized with a reference count + * of 1. + * + * @param data the source buffer + * @param size the size of the source buffer + */ + static Page *Copy(const void *data, size_t size); + + /** + * Concatenates two pages to a new page. + * + * @param a the first page + * @param b the second page, which is appended + */ + static Page *Concat(const Page &a, const Page &b); + + /** + * Increases the reference counter. + */ + void Ref() { + ref.Increment(); + } + + /** + * Decreases the reference counter. If it reaches zero, the #page is + * freed. + * + * @return true if the #page has been freed + */ + bool Unref(); +}; + +#endif -- cgit v1.2.3