HttpdOutputPlugin.cxx 9.29 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2017 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
#include "encoder/Configured.hxx"
27
#include "net/UniqueSocketDescriptor.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 "event/Call.hxx"
33
#include "util/Domain.hxx"
34
#include "util/DeleteDisposer.hxx"
35
#include "Log.hxx"
36 37 38

#include <assert.h>

Max Kellermann's avatar
Max Kellermann committed
39
#include <string.h>
40 41
#include <errno.h>

42
#ifdef HAVE_LIBWRAP
43
#include <sys/socket.h> /* needed for AF_LOCAL */
44 45 46
#include <tcpd.h>
#endif

47
const Domain httpd_output_domain("httpd_output");
48

49
inline
50
HttpdOutput::HttpdOutput(EventLoop &_loop, const ConfigBlock &block)
51
	:AudioOutput(FLAG_ENABLE_DISABLE|FLAG_PAUSE),
52
	 ServerSocket(_loop),
53
	 prepared_encoder(CreateConfiguredEncoder(block)),
54
	 defer_broadcast(_loop, BIND_THIS_METHOD(OnDeferredBroadcast))
55 56
{
	/* read configuration */
57 58 59
	name = block.GetBlockValue("name", "Set name in config");
	genre = block.GetBlockValue("genre", "Set genre in config");
	website = block.GetBlockValue("website", "Set website in config");
60

61
	unsigned port = block.GetBlockValue("port", 8000u);
62

63
	clients_max = block.GetBlockValue("max_clients", 0u);
64

65
	/* set up bind_to_address */
66

67
	const char *bind_to_address = block.GetBlockValue("bind_to_address");
68 69 70 71
	if (bind_to_address != nullptr && strcmp(bind_to_address, "any") != 0)
		AddHost(bind_to_address, port);
	else
		AddPort(port);
72

73
	/* determine content type */
74
	content_type = prepared_encoder->GetMimeType();
75 76
	if (content_type == nullptr)
		content_type = "application/octet-stream";
77 78 79 80
}

inline void
HttpdOutput::Bind()
81
{
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
	open = false;

	BlockingCall(GetEventLoop(), [this](){
			ServerSocket::Open();
		});
}

inline void
HttpdOutput::Unbind()
{
	assert(!open);

	BlockingCall(GetEventLoop(), [this](){
			ServerSocket::Close();
		});
97 98
}

99
/**
100
 * Creates a new #HttpdClient object and adds it into the
101
 * HttpdOutput.clients linked list.
102
 */
103
inline void
104
HttpdOutput::AddClient(UniqueSocketDescriptor fd)
105
{
106
	auto *client = new HttpdClient(*this, std::move(fd), GetEventLoop(),
107
				       !encoder->ImplementsTag());
108
	clients.push_front(*client);
109 110

	/* pass metadata to client */
111 112
	if (metadata != nullptr)
		clients.front().PushMetaData(metadata);
113 114
}

115
void
116
HttpdOutput::OnDeferredBroadcast() noexcept
117 118 119 120
{
	/* this method runs in the IOThread; it broadcasts pages from
	   our own queue to all clients */

121
	const std::lock_guard<Mutex> protect(mutex);
122 123

	while (!pages.empty()) {
124
		PagePtr page = std::move(pages.front());
125 126 127 128 129 130 131 132 133 134 135
		pages.pop();

		for (auto &client : clients)
			client.PushPage(page);
	}

	/* wake up the client that may be waiting for the queue to be
	   flushed */
	cond.broadcast();
}

136
void
137
HttpdOutput::OnAccept(UniqueSocketDescriptor fd,
138
		      SocketAddress address, gcc_unused int uid)
139 140 141 142
{
	/* the listener socket has become readable - a client has
	   connected */

143
#ifdef HAVE_LIBWRAP
144
	if (address.GetFamily() != AF_LOCAL) {
145
		const auto hostaddr = ToString(address);
146 147
		// TODO: shall we obtain the program name from argv[0]?
		const char *progname = "mpd";
148 149

		struct request_info req;
150
		request_init(&req, RQ_FILE, fd.Get(), RQ_DAEMON, progname, 0);
151 152 153 154 155

		fromhost(&req);

		if (!hosts_access(&req)) {
			/* tcp wrappers says no */
156 157
			FormatWarning(httpd_output_domain,
				      "libwrap refused connection (libwrap=%s) from %s",
158
				      progname, hostaddr.c_str());
159
			return;
160 161
		}
	}
162 163
#else
	(void)address;
164
#endif	/* HAVE_WRAP */
165

166
	const std::lock_guard<Mutex> protect(mutex);
167

168 169
	/* can we allow additional client */
	if (open && (clients_max == 0 || clients.size() < clients_max))
170
		AddClient(std::move(fd));
171 172
}

173
PagePtr
174
HttpdOutput::ReadPage()
175
{
176
	if (unflushed_input >= 65536) {
177 178 179
		/* we have fed a lot of input into the encoder, but it
		   didn't give anything back yet - flush now to avoid
		   buffer underruns */
180 181
		try {
			encoder->Flush();
182
		} catch (...) {
183 184 185
			/* ignore */
		}

186
		unflushed_input = 0;
187 188
	}

189
	size_t size = 0;
190
	do {
191 192
		size_t nbytes = encoder->Read(buffer + size,
					      sizeof(buffer) - size);
193 194 195
		if (nbytes == 0)
			break;

196
		unflushed_input = 0;
197

198
		size += nbytes;
199
	} while (size < sizeof(buffer));
200 201

	if (size == 0)
202
		return nullptr;
203

204
	return std::make_shared<Page>(buffer, size);
205 206
}

207 208
inline void
HttpdOutput::OpenEncoder(AudioFormat &audio_format)
209
{
210
	encoder = prepared_encoder->Open(audio_format);
211 212 213 214

	/* 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 */
215
	header = ReadPage();
216

217
	unflushed_input = 0;
218 219
}

220
void
221
HttpdOutput::Open(AudioFormat &audio_format)
222
{
223 224
	assert(!open);
	assert(clients.empty());
225

226 227
	const std::lock_guard<Mutex> protect(mutex);

228
	OpenEncoder(audio_format);
229 230 231

	/* initialize other attributes */

232
	timer = new Timer(audio_format);
233

234
	open = true;
235
	pause = false;
236 237
}

238 239
void
HttpdOutput::Close() noexcept
240
{
241
	assert(open);
242

243
	delete timer;
244

245
	BlockingCall(GetEventLoop(), [this](){
246
			defer_broadcast.Cancel();
247

248 249
			const std::lock_guard<Mutex> protect(mutex);
			open = false;
250
			clients.clear_and_dispose(DeleteDisposer());
251
		});
252

253
	header.reset();
254

255
	delete encoder;
256 257 258
}

void
259
HttpdOutput::RemoveClient(HttpdClient &client)
260
{
261 262 263 264
	assert(!clients.empty());

	clients.erase_and_dispose(clients.iterator_to(client),
				  DeleteDisposer());
265 266 267
}

void
268
HttpdOutput::SendHeader(HttpdClient &client) const
269
{
270
	if (header != nullptr)
271
		client.PushPage(header);
272 273
}

274
std::chrono::steady_clock::duration
275
HttpdOutput::Delay() const noexcept
276
{
277
	if (!LockHasClients() && pause) {
278 279 280 281
		/* 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 */
282
		timer->Reset();
283 284 285 286

		/* some arbitrary delay that is long enough to avoid
		   consuming too much CPU, and short enough to notice
		   new clients quickly enough */
287
		return std::chrono::seconds(1);
288 289
	}

290
	return timer->IsStarted()
291
		? timer->GetDelay()
292
		: std::chrono::steady_clock::duration::zero();
293 294
}

295
void
296
HttpdOutput::BroadcastPage(PagePtr page)
297
{
298
	assert(page != nullptr);
299

300 301 302 303
	{
		const std::lock_guard<Mutex> lock(mutex);
		pages.emplace(std::move(page));
	}
304

305
	defer_broadcast.Schedule();
306 307
}

308 309
void
HttpdOutput::BroadcastFromEncoder()
310
{
311
	/* synchronize with the IOThread */
312 313 314 315
	{
		const std::lock_guard<Mutex> lock(mutex);
		while (!pages.empty())
			cond.wait(mutex);
316
	}
317

318 319
	bool empty = true;

320 321 322
	PagePtr page;
	while ((page = ReadPage()) != nullptr) {
		const std::lock_guard<Mutex> lock(mutex);
323
		pages.emplace(std::move(page));
324
		empty = false;
325
	}
326

327
	if (!empty)
328
		defer_broadcast.Schedule();
329 330
}

331 332
inline void
HttpdOutput::EncodeAndPlay(const void *chunk, size_t size)
333
{
334
	encoder->Write(chunk, size);
335

336
	unflushed_input += size;
337

338
	BroadcastFromEncoder();
339 340
}

341
size_t
342
HttpdOutput::Play(const void *chunk, size_t size)
343
{
344 345
	pause = false;

346 347
	if (LockHasClients())
		EncodeAndPlay(chunk, size);
348

349 350 351
	if (!timer->IsStarted())
		timer->Start();
	timer->Add(size);
352 353 354 355

	return size;
}

356
bool
357
HttpdOutput::Pause()
358
{
359 360
	pause = true;

361
	if (LockHasClients()) {
362
		static const char silence[1020] = { 0 };
363
		Play(silence, sizeof(silence));
364
	}
365 366

	return true;
367 368
}

369
void
370
HttpdOutput::SendTag(const Tag &tag)
371
{
372
	if (encoder->ImplementsTag()) {
373
		/* embed encoder tags */
374 375 376

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

377 378
		try {
			encoder->PreTag();
379
		} catch (...) {
380 381 382
			/* ignore */
		}

383
		BroadcastFromEncoder();
384 385 386

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

388 389
		try {
			encoder->SendTag(tag);
390
			encoder->Flush();
391
		} catch (...) {
392 393
			/* ignore */
		}
394 395 396 397 398

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

399
		auto page = ReadPage();
400
		if (page != nullptr) {
401
			header = page;
402
			BroadcastPage(page);
403
		}
404 405 406
	} else {
		/* use Icy-Metadata */

407
		static constexpr TagType types[] = {
408 409 410 411
			TAG_ALBUM, TAG_ARTIST, TAG_TITLE,
			TAG_NUM_OF_ITEM_TYPES
		};

412
		metadata = icy_server_metadata_page(tag, &types[0]);
413
		if (metadata != nullptr) {
414
			const std::lock_guard<Mutex> protect(mutex);
415 416
			for (auto &client : clients)
				client.PushMetaData(metadata);
417
		}
418
	}
419 420
}

421 422 423
inline void
HttpdOutput::CancelAllClients()
{
424
	const std::lock_guard<Mutex> protect(mutex);
425 426

	while (!pages.empty()) {
427
		PagePtr page = std::move(pages.front());
428 429 430
		pages.pop();
	}

431 432
	for (auto &client : clients)
		client.CancelQueue();
433 434

	cond.broadcast();
435 436
}

437
void
438
HttpdOutput::Cancel() noexcept
439
{
440
	BlockingCall(GetEventLoop(), [this](){
441
			CancelAllClients();
442
		});
443 444
}

445
const struct AudioOutputPlugin httpd_output_plugin = {
446 447
	"httpd",
	nullptr,
448
	&HttpdOutput::Create,
449
	nullptr,
450
};