HttpdOutputPlugin.cxx 12 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright (C) 2003-2015 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
#include "encoder/EncoderInterface.hxx"
26 27
#include "encoder/EncoderPlugin.hxx"
#include "encoder/EncoderList.hxx"
28
#include "net/SocketAddress.hxx"
29
#include "net/ToString.hxx"
Max Kellermann's avatar
Max Kellermann committed
30
#include "Page.hxx"
31
#include "IcyMetaDataServer.hxx"
32
#include "system/fd_util.h"
33
#include "IOThread.hxx"
34
#include "event/Call.hxx"
35 36
#include "util/Error.hxx"
#include "util/Domain.hxx"
37
#include "Log.hxx"
38 39 40

#include <assert.h>

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

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

51
const Domain httpd_output_domain("httpd_output");
52

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

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

	if (encoder != nullptr)
68
		encoder->Dispose();
69 70 71

}

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

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

84 85
inline void
HttpdOutput::Unbind()
86
{
87
	assert(!open);
88

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

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

102
	unsigned port = block.GetBlockValue("port", 8000u);
103

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

113
	clients_max = block.GetBlockValue("max_clients", 0u);
114

115
	/* set up bind_to_address */
116

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

125 126
	/* initialize encoder */

127
	encoder = encoder_init(*encoder_plugin, block, error);
128 129
	if (encoder == nullptr)
		return false;
130

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

	return true;
}

139
inline bool
140
HttpdOutput::Init(const ConfigBlock &block, Error &error)
141
{
142
	return base.Configure(block, error);
143 144
}

145
static AudioOutput *
146
httpd_output_init(const ConfigBlock &block, Error &error)
147
{
148
	HttpdOutput *httpd = new HttpdOutput(io_thread_get());
149

150
	AudioOutput *result = httpd->InitAndConfigure(block, error);
151
	if (result == nullptr)
152
		delete httpd;
153

154
	return result;
155 156 157
}

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

162
	delete httpd;
163 164 165
}

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

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

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

204
void
205
HttpdOutput::OnAccept(int fd, SocketAddress address, gcc_unused int uid)
206 207 208 209
{
	/* the listener socket has become readable - a client has
	   connected */

210
#ifdef HAVE_LIBWRAP
211
	if (address.GetFamily() != AF_UNIX) {
212
		const auto hostaddr = ToString(address);
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
#else
	(void)address;
232
#endif	/* HAVE_WRAP */
233

234
	const ScopeLock protect(mutex);
235

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

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

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

266
		unflushed_input = 0;
267

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

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

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

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

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

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

	httpd->Unbind();
}

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

304
	unflushed_input = 0;
305

306 307 308
	return true;
}

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

315
	/* open the encoder */
316

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

	/* initialize other attributes */

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

325 326 327
	open = true;

	return true;
328 329 330
}

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

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

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

345
	open = false;
346

347
	delete timer;
348

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

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

356
	encoder->Close();
357 358
}

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

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

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

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

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

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

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

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

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

	return httpd->Delay();
}

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

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

	DeferredMonitor::Schedule();
431 432
}

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

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

	mutex.unlock();

	DeferredMonitor::Schedule();
448 449
}

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

456
	unflushed_input += size;
457

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

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

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

	return size;
}

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

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

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

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

500
inline void
501
HttpdOutput::SendTag(const Tag &tag)
502
{
503
	if (encoder->plugin.tag != nullptr) {
504
		/* embed encoder tags */
505 506 507

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

508
		encoder_pre_tag(encoder, IgnoreError());
509
		BroadcastFromEncoder();
510 511 512

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

514
		encoder_tag(encoder, tag, IgnoreError());
515 516 517 518 519

		/* 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
520
		Page *page = ReadPage();
521 522
		if (page != nullptr) {
			if (header != nullptr)
Max Kellermann's avatar
Max Kellermann committed
523
				header->Unref();
524 525
			header = page;
			BroadcastPage(page);
526
		}
527 528 529
	} else {
		/* use Icy-Metadata */

530
		if (metadata != nullptr)
Max Kellermann's avatar
Max Kellermann committed
531
			metadata->Unref();
532

533
		static constexpr TagType types[] = {
534 535 536 537
			TAG_ALBUM, TAG_ARTIST, TAG_TITLE,
			TAG_NUM_OF_ITEM_TYPES
		};

538
		metadata = icy_server_metadata_page(tag, &types[0]);
539
		if (metadata != nullptr) {
540 541 542
			const ScopeLock protect(mutex);
			for (auto &client : clients)
				client.PushMetaData(metadata);
543
		}
544
	}
545 546
}

547
static void
548
httpd_output_tag(AudioOutput *ao, const Tag &tag)
549
{
550
	HttpdOutput *httpd = HttpdOutput::Cast(ao);
551 552 553 554

	httpd->SendTag(tag);
}

555 556 557 558
inline void
HttpdOutput::CancelAllClients()
{
	const ScopeLock protect(mutex);
559 560 561 562 563 564 565

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

566 567
	for (auto &client : clients)
		client.CancelQueue();
568 569

	cond.broadcast();
570 571
}

572
static void
573
httpd_output_cancel(AudioOutput *ao)
574
{
575
	HttpdOutput *httpd = HttpdOutput::Cast(ao);
576 577 578 579

	BlockingCall(io_thread_get(), [httpd](){
			httpd->CancelAllClients();
		});
580 581
}

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