HttpdOutputPlugin.cxx 12.1 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright (C) 2003-2014 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 * 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.
 */

20
#include "config.h"
21 22 23
#include "HttpdOutputPlugin.hxx"
#include "HttpdInternal.hxx"
#include "HttpdClient.hxx"
24
#include "output/OutputAPI.hxx"
25 26
#include "encoder/EncoderPlugin.hxx"
#include "encoder/EncoderList.hxx"
27
#include "system/Resolver.hxx"
Max Kellermann's avatar
Max Kellermann committed
28
#include "Page.hxx"
29
#include "IcyMetaDataServer.hxx"
30
#include "system/fd_util.h"
31
#include "IOThread.hxx"
32
#include "event/Call.hxx"
33 34
#include "util/Error.hxx"
#include "util/Domain.hxx"
35
#include "Log.hxx"
36 37 38

#include <assert.h>

39
#include <sys/types.h>
40
#include <unistd.h>
Max Kellermann's avatar
Max Kellermann committed
41
#include <string.h>
42 43
#include <errno.h>

44
#ifdef HAVE_LIBWRAP
45
#include <sys/socket.h> /* needed for AF_UNIX */
46 47 48
#include <tcpd.h>
#endif

49
const Domain httpd_output_domain("httpd_output");
50

51 52
inline
HttpdOutput::HttpdOutput(EventLoop &_loop)
53
	:ServerSocket(_loop), DeferredMonitor(_loop),
54
	 base(httpd_output_plugin),
55
	 encoder(nullptr), unflushed_input(0),
56 57 58 59 60 61 62 63 64 65 66 67 68 69
	 metadata(nullptr)
{
}

HttpdOutput::~HttpdOutput()
{
	if (metadata != nullptr)
		metadata->Unref();

	if (encoder != nullptr)
		encoder_finish(encoder);

}

70
inline bool
71
HttpdOutput::Bind(Error &error)
72
{
73
	open = false;
74

75
	bool result = false;
76
	BlockingCall(GetEventLoop(), [this, &error, &result](){
77 78 79
			result = ServerSocket::Open(error);
		});
	return result;
80 81
}

82 83
inline void
HttpdOutput::Unbind()
84
{
85
	assert(!open);
86

87
	BlockingCall(GetEventLoop(), [this](){
88 89
			ServerSocket::Close();
		});
90 91
}

92
inline bool
93
HttpdOutput::Configure(const config_param &param, Error &error)
94 95
{
	/* read configuration */
96 97 98
	name = param.GetBlockValue("name", "Set name in config");
	genre = param.GetBlockValue("genre", "Set genre in config");
	website = param.GetBlockValue("website", "Set website in config");
99

100
	unsigned port = param.GetBlockValue("port", 8000u);
101

102
	const char *encoder_name =
103
		param.GetBlockValue("encoder", "vorbis");
104
	const auto encoder_plugin = encoder_plugin_get(encoder_name);
105
	if (encoder_plugin == nullptr) {
106 107
		error.Format(httpd_output_domain,
			     "No such encoder: %s", encoder_name);
108
		return false;
109 110
	}

111
	clients_max = param.GetBlockValue("max_clients", 0u);
112

113
	/* set up bind_to_address */
114

115
	const char *bind_to_address = param.GetBlockValue("bind_to_address");
116
	bool success = bind_to_address != nullptr &&
117
		strcmp(bind_to_address, "any") != 0
118 119
		? AddHost(bind_to_address, port, error)
		: AddPort(port, error);
120 121
	if (!success)
		return false;
122

123 124
	/* initialize encoder */

125
	encoder = encoder_init(*encoder_plugin, param, error);
126 127
	if (encoder == nullptr)
		return false;
128

129
	/* determine content type */
130 131 132 133 134 135 136
	content_type = encoder_get_mime_type(encoder);
	if (content_type == nullptr)
		content_type = "application/octet-stream";

	return true;
}

137 138 139
inline bool
HttpdOutput::Init(const config_param &param, Error &error)
{
140
	return base.Configure(param, error);
141 142
}

143
static AudioOutput *
144
httpd_output_init(const config_param &param, Error &error)
145
{
146
	HttpdOutput *httpd = new HttpdOutput(io_thread_get());
147

148
	AudioOutput *result = httpd->InitAndConfigure(param, error);
149
	if (result == nullptr)
150
		delete httpd;
151

152
	return result;
153 154 155
}

static void
156
httpd_output_finish(AudioOutput *ao)
157
{
158
	HttpdOutput *httpd = HttpdOutput::Cast(ao);
159

160
	delete httpd;
161 162 163
}

/**
164
 * Creates a new #HttpdClient object and adds it into the
165
 * HttpdOutput.clients linked list.
166
 */
167 168
inline void
HttpdOutput::AddClient(int fd)
169
{
170
	clients.emplace_front(*this, fd, GetEventLoop(),
171
			      encoder->plugin.tag == nullptr);
172
	++clients_cnt;
173 174

	/* pass metadata to client */
175 176
	if (metadata != nullptr)
		clients.front().PushMetaData(metadata);
177 178
}

179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
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();
}

202 203 204
void
HttpdOutput::OnAccept(int fd, const sockaddr &address,
		      size_t address_length, gcc_unused int uid)
205 206 207 208
{
	/* the listener socket has become readable - a client has
	   connected */

209
#ifdef HAVE_LIBWRAP
210
	if (address.sa_family != AF_UNIX) {
211 212
		const auto hostaddr = sockaddr_to_string(&address,
							 address_length);
213 214
		// TODO: shall we obtain the program name from argv[0]?
		const char *progname = "mpd";
215 216 217 218 219 220 221 222

		struct request_info req;
		request_init(&req, RQ_FILE, fd, RQ_DAEMON, progname, 0);

		fromhost(&req);

		if (!hosts_access(&req)) {
			/* tcp wrappers says no */
223 224
			FormatWarning(httpd_output_domain,
				      "libwrap refused connection (libwrap=%s) from %s",
225
				      progname, hostaddr.c_str());
226
			close_socket(fd);
227
			return;
228 229
		}
	}
230 231 232
#else
	(void)address;
	(void)address_length;
233
#endif	/* HAVE_WRAP */
234

235
	const ScopeLock protect(mutex);
236

237 238
	if (fd >= 0) {
		/* can we allow additional client */
239 240
		if (open && (clients_max == 0 ||  clients_cnt < clients_max))
			AddClient(fd);
241
		else
242
			close_socket(fd);
243
	} else if (fd < 0 && errno != EINTR) {
244
		LogErrno(httpd_output_domain, "accept() failed");
245
	}
246 247
}

Max Kellermann's avatar
Max Kellermann committed
248
Page *
249
HttpdOutput::ReadPage()
250
{
251
	if (unflushed_input >= 65536) {
252 253 254
		/* we have fed a lot of input into the encoder, but it
		   didn't give anything back yet - flush now to avoid
		   buffer underruns */
255
		encoder_flush(encoder, IgnoreError());
256
		unflushed_input = 0;
257 258
	}

259
	size_t size = 0;
260
	do {
261 262 263
		size_t nbytes = encoder_read(encoder,
					     buffer + size,
					     sizeof(buffer) - size);
264 265 266
		if (nbytes == 0)
			break;

267
		unflushed_input = 0;
268

269
		size += nbytes;
270
	} while (size < sizeof(buffer));
271 272

	if (size == 0)
273
		return nullptr;
274

Max Kellermann's avatar
Max Kellermann committed
275
	return Page::Copy(buffer, size);
276 277 278
}

static bool
279
httpd_output_enable(AudioOutput *ao, Error &error)
280
{
281
	HttpdOutput *httpd = HttpdOutput::Cast(ao);
282

283
	return httpd->Bind(error);
284 285 286
}

static void
287
httpd_output_disable(AudioOutput *ao)
288
{
289
	HttpdOutput *httpd = HttpdOutput::Cast(ao);
290 291 292 293 294

	httpd->Unbind();
}

inline bool
295
HttpdOutput::OpenEncoder(AudioFormat &audio_format, Error &error)
296 297
{
	if (!encoder_open(encoder, audio_format, error))
298 299 300 301 302
		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 */
303
	header = ReadPage();
304

305
	unflushed_input = 0;
306

307 308 309
	return true;
}

310
inline bool
311
HttpdOutput::Open(AudioFormat &audio_format, Error &error)
312
{
313 314
	assert(!open);
	assert(clients.empty());
315

316
	/* open the encoder */
317

318
	if (!OpenEncoder(audio_format, error))
319 320 321 322 323
		return false;

	/* initialize other attributes */

	clients_cnt = 0;
324
	timer = new Timer(audio_format);
325

326 327 328
	open = true;

	return true;
329 330 331
}

static bool
332
httpd_output_open(AudioOutput *ao, AudioFormat &audio_format,
333
		  Error &error)
334
{
335
	HttpdOutput *httpd = HttpdOutput::Cast(ao);
336 337

	const ScopeLock protect(httpd->mutex);
338 339
	return httpd->Open(audio_format, error);
}
340

341 342 343 344
inline void
HttpdOutput::Close()
{
	assert(open);
345

346
	open = false;
347

348
	delete timer;
349

350 351 352
	BlockingCall(GetEventLoop(), [this](){
			clients.clear();
		});
353

354
	if (header != nullptr)
Max Kellermann's avatar
Max Kellermann committed
355
		header->Unref();
356

357
	encoder_close(encoder);
358 359
}

360
static void
361
httpd_output_close(AudioOutput *ao)
362
{
363
	HttpdOutput *httpd = HttpdOutput::Cast(ao);
364

365
	const ScopeLock protect(httpd->mutex);
366
	httpd->Close();
367 368 369
}

void
370
HttpdOutput::RemoveClient(HttpdClient &client)
371
{
372
	assert(clients_cnt > 0);
373

374
	for (auto prev = clients.before_begin(), i = std::next(prev);;
375
	     prev = i, i = std::next(prev)) {
376 377 378 379
		assert(i != clients.end());
		if (&*i == &client) {
			clients.erase_after(prev);
			clients_cnt--;
380 381 382
			break;
		}
	}
383 384 385
}

void
386
HttpdOutput::SendHeader(HttpdClient &client) const
387
{
388
	if (header != nullptr)
389
		client.PushPage(header);
390 391
}

392 393
inline unsigned
HttpdOutput::Delay() const
394
{
395
	if (!LockHasClients() && base.pause) {
396 397 398 399
		/* 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 */
400
		timer->Reset();
401 402 403 404 405

		/* some arbitrary delay that is long enough to avoid
		   consuming too much CPU, and short enough to notice
		   new clients quickly enough */
		return 1000;
406 407
	}

408 409
	return timer->IsStarted()
		? timer->GetDelay()
410 411 412
		: 0;
}

413
static unsigned
414
httpd_output_delay(AudioOutput *ao)
415 416 417 418 419 420
{
	HttpdOutput *httpd = HttpdOutput::Cast(ao);

	return httpd->Delay();
}

421
void
Max Kellermann's avatar
Max Kellermann committed
422
HttpdOutput::BroadcastPage(Page *page)
423
{
424
	assert(page != nullptr);
425

426 427 428 429 430 431
	mutex.lock();
	pages.push(page);
	page->Ref();
	mutex.unlock();

	DeferredMonitor::Schedule();
432 433
}

434 435
void
HttpdOutput::BroadcastFromEncoder()
436
{
437 438 439 440 441
	/* synchronize with the IOThread */
	mutex.lock();
	while (!pages.empty())
		cond.wait(mutex);

Max Kellermann's avatar
Max Kellermann committed
442
	Page *page;
443 444 445 446 447 448
	while ((page = ReadPage()) != nullptr)
		pages.push(page);

	mutex.unlock();

	DeferredMonitor::Schedule();
449 450
}

451
inline bool
452
HttpdOutput::EncodeAndPlay(const void *chunk, size_t size, Error &error)
453
{
454
	if (!encoder_write(encoder, chunk, size, error))
455 456
		return false;

457
	unflushed_input += size;
458

459
	BroadcastFromEncoder();
460 461 462
	return true;
}

463 464
inline size_t
HttpdOutput::Play(const void *chunk, size_t size, Error &error)
465
{
466 467
	if (LockHasClients()) {
		if (!EncodeAndPlay(chunk, size, error))
468 469 470
			return 0;
	}

471 472 473
	if (!timer->IsStarted())
		timer->Start();
	timer->Add(size);
474 475 476 477

	return size;
}

478
static size_t
479
httpd_output_play(AudioOutput *ao, const void *chunk, size_t size,
480 481 482 483 484 485 486
		  Error &error)
{
	HttpdOutput *httpd = HttpdOutput::Cast(ao);

	return httpd->Play(chunk, size, error);
}

487
static bool
488
httpd_output_pause(AudioOutput *ao)
489
{
490
	HttpdOutput *httpd = HttpdOutput::Cast(ao);
491

492
	if (httpd->LockHasClients()) {
493
		static const char silence[1020] = { 0 };
494
		return httpd_output_play(ao, silence, sizeof(silence),
495
					 IgnoreError()) > 0;
496 497 498 499 500
	} else {
		return true;
	}
}

501
inline void
Max Kellermann's avatar
Max Kellermann committed
502
HttpdOutput::SendTag(const Tag *tag)
503
{
504
	assert(tag != nullptr);
505

506
	if (encoder->plugin.tag != nullptr) {
507
		/* embed encoder tags */
508 509 510

		/* flush the current stream, and end it */

511
		encoder_pre_tag(encoder, IgnoreError());
512
		BroadcastFromEncoder();
513 514 515

		/* send the tag to the encoder - which starts a new
		   stream now */
516

517
		encoder_tag(encoder, tag, IgnoreError());
518 519 520 521 522

		/* the first page generated by the encoder will now be
		   used as the new "header" page, which is sent to all
		   new clients */

Max Kellermann's avatar
Max Kellermann committed
523
		Page *page = ReadPage();
524 525
		if (page != nullptr) {
			if (header != nullptr)
Max Kellermann's avatar
Max Kellermann committed
526
				header->Unref();
527 528
			header = page;
			BroadcastPage(page);
529
		}
530 531 532
	} else {
		/* use Icy-Metadata */

533
		if (metadata != nullptr)
Max Kellermann's avatar
Max Kellermann committed
534
			metadata->Unref();
535

536
		static constexpr TagType types[] = {
537 538 539 540
			TAG_ALBUM, TAG_ARTIST, TAG_TITLE,
			TAG_NUM_OF_ITEM_TYPES
		};

Max Kellermann's avatar
Max Kellermann committed
541
		metadata = icy_server_metadata_page(*tag, &types[0]);
542
		if (metadata != nullptr) {
543 544 545
			const ScopeLock protect(mutex);
			for (auto &client : clients)
				client.PushMetaData(metadata);
546
		}
547
	}
548 549
}

550
static void
551
httpd_output_tag(AudioOutput *ao, const Tag *tag)
552
{
553
	HttpdOutput *httpd = HttpdOutput::Cast(ao);
554 555 556 557

	httpd->SendTag(tag);
}

558 559 560 561
inline void
HttpdOutput::CancelAllClients()
{
	const ScopeLock protect(mutex);
562 563 564 565 566 567 568

	while (!pages.empty()) {
		Page *page = pages.front();
		pages.pop();
		page->Unref();
	}

569 570
	for (auto &client : clients)
		client.CancelQueue();
571 572

	cond.broadcast();
573 574
}

575
static void
576
httpd_output_cancel(AudioOutput *ao)
577
{
578
	HttpdOutput *httpd = HttpdOutput::Cast(ao);
579 580 581 582

	BlockingCall(io_thread_get(), [httpd](){
			httpd->CancelAllClients();
		});
583 584
}

585
const struct AudioOutputPlugin httpd_output_plugin = {
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600
	"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,
601
};