Users now have to call Close() explicitly. This simplifies using the class, as most users have automatic socket management already, and Steal() had to be used often.
486 lines
9.7 KiB
C++
486 lines
9.7 KiB
C++
/*
|
|
* Copyright (C) 2003-2013 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 <glib.h>
|
|
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
|
|
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();
|
|
}
|