CurlInputPlugin.cxx 12.8 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2021 The Music Player Daemon Project
3
 * http://www.musicpd.org
Max Kellermann's avatar
Max Kellermann committed
4 5 6 7 8 9 10 11 12 13
 *
 * 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.
14 15 16 17
 *
 * 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.
Max Kellermann's avatar
Max Kellermann committed
18 19
 */

20
#include "CurlInputPlugin.hxx"
21
#include "lib/curl/Error.hxx"
22
#include "lib/curl/Global.hxx"
23
#include "lib/curl/Init.hxx"
24
#include "lib/curl/Request.hxx"
25
#include "lib/curl/Handler.hxx"
26
#include "lib/curl/Slist.hxx"
27
#include "../MaybeBufferedInputStream.hxx"
28
#include "../AsyncInputStream.hxx"
29
#include "../IcyInputStream.hxx"
30
#include "IcyMetaDataParser.hxx"
Max Kellermann's avatar
Max Kellermann committed
31
#include "../InputPlugin.hxx"
32
#include "config/Block.hxx"
33
#include "tag/Builder.hxx"
34
#include "tag/Tag.hxx"
35
#include "event/Call.hxx"
36
#include "event/Loop.hxx"
37
#include "util/ASCII.hxx"
38
#include "util/StringFormat.hxx"
39
#include "util/StringView.hxx"
40
#include "util/NumberParser.hxx"
41
#include "util/Domain.hxx"
42
#include "Log.hxx"
43
#include "PluginUnavailable.hxx"
44 45 46 47 48 49 50 51
#include "config.h"

#ifdef HAVE_ICU_CONVERTER
#include "lib/icu/Converter.hxx"
#include "util/AllocatedString.hxx"
#include "util/UriExtract.hxx"
#include "util/UriQueryParser.hxx"
#endif
Max Kellermann's avatar
Max Kellermann committed
52

53
#include <cassert>
54 55
#include <cinttypes>

Max Kellermann's avatar
Max Kellermann committed
56
#include <string.h>
57

Max Kellermann's avatar
Max Kellermann committed
58 59
#include <curl/curl.h>

60 61 62 63 64 65 66
/**
 * Do not buffer more than this number of bytes.  It should be a
 * reasonable limit that doesn't make low-end machines suffer too
 * much, but doesn't cause stuttering on high-latency lines.
 */
static const size_t CURL_MAX_BUFFERED = 512 * 1024;

67 68 69 70 71
/**
 * Resume the stream at this number of bytes after it has been paused.
 */
static const size_t CURL_RESUME_AT = 384 * 1024;

72
class CurlInputStream final : public AsyncInputStream, CurlResponseHandler {
Max Kellermann's avatar
Max Kellermann committed
73 74
	/* some buffers which were passed to libcurl, which we have
	   too free */
75
	CurlSlist request_headers;
Max Kellermann's avatar
Max Kellermann committed
76

77
	CurlRequest *request = nullptr;
78

79
	/** parser for icy-metadata */
80
	std::shared_ptr<IcyMetaDataParser> icy;
81

82
public:
83
	template<typename I>
84
	CurlInputStream(EventLoop &event_loop, const char *_url,
85
			const std::multimap<std::string, std::string> &headers,
86
			I &&_icy,
87
			Mutex &_mutex);
88

89
	~CurlInputStream() noexcept override;
90

91 92
	CurlInputStream(const CurlInputStream &) = delete;
	CurlInputStream &operator=(const CurlInputStream &) = delete;
93

94 95
	static InputStreamPtr Open(const char *url,
				   const std::multimap<std::string, std::string> &headers,
96
				   Mutex &mutex);
97

98
private:
99 100 101 102 103
	/**
	 * Create and initialize a new #CurlRequest instance.  After
	 * this, you may add more request headers and set options.  To
	 * actually start the request, call StartRequest().
	 */
104
	void InitEasy();
105

106 107 108 109 110 111
	/**
	 * Start the request after having called InitEasy().  After
	 * this, you must not set any CURL options.
	 */
	void StartRequest();

112 113 114 115 116 117
	/**
	 * Frees the current "libcurl easy" handle, and everything
	 * associated with it.
	 *
	 * Runs in the I/O thread.
	 */
118
	void FreeEasy() noexcept;
119 120 121 122 123 124 125

	/**
	 * Frees the current "libcurl easy" handle, and everything associated
	 * with it.
	 *
	 * The mutex must not be locked.
	 */
126
	void FreeEasyIndirect() noexcept;
127

128 129 130 131 132
	/**
	 * The DoSeek() implementation invoked in the IOThread.
	 */
	void SeekInternal(offset_type new_offset);

133 134 135 136 137
	/* virtual methods from CurlResponseHandler */
	void OnHeaders(unsigned status,
		       std::multimap<std::string, std::string> &&headers) override;
	void OnData(ConstBuffer<void> data) override;
	void OnEnd() override;
138
	void OnError(std::exception_ptr e) noexcept override;
139

140
	/* virtual methods from AsyncInputStream */
141 142
	void DoResume() override;
	void DoSeek(offset_type new_offset) override;
Max Kellermann's avatar
Max Kellermann committed
143 144 145 146 147
};

/** libcurl should accept "ICY 200 OK" */
static struct curl_slist *http_200_aliases;

148 149 150
/** HTTP proxy settings */
static const char *proxy, *proxy_user, *proxy_password;
static unsigned proxy_port;
151 152
/** CA CERT settings*/
static const char *cacert;
153

154 155
static bool verify_peer, verify_host;

156
static CurlInit *curl_init;
157

158
static constexpr Domain curl_domain("curl");
159

160 161
void
CurlInputStream::DoResume()
162
{
163
	assert(GetEventLoop().IsInside());
164

165
	const ScopeUnlock unlock(mutex);
166
	request->Resume();
167 168
}

169
void
170
CurlInputStream::FreeEasy() noexcept
171
{
172
	assert(GetEventLoop().IsInside());
173

174
	if (request == nullptr)
175 176
		return;

177 178
	delete request;
	request = nullptr;
179 180
}

181
void
182
CurlInputStream::FreeEasyIndirect() noexcept
183
{
184
	BlockingCall(GetEventLoop(), [this](){
185
			FreeEasy();
186
		});
187 188
}

189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
#ifdef HAVE_ICU_CONVERTER

static std::unique_ptr<IcuConverter>
CreateIcuConverterForUri(const char *uri)
{
	const char *fragment = uri_get_fragment(uri);
	if (fragment == nullptr)
		return nullptr;

	const auto charset = UriFindRawQueryParameter(fragment, "charset");
	if (charset == nullptr)
		return nullptr;

	const std::string copy(charset.data, charset.size);
	return IcuConverter::Create(copy.c_str());
}

#endif

template<typename F>
static void
WithConvertedTagValue(const char *uri, const char *value, F &&f) noexcept
{
#ifdef HAVE_ICU_CONVERTER
	try {
		auto converter = CreateIcuConverterForUri(uri);
		if (converter) {
			f(converter->ToUTF8(value).c_str());
			return;
		}
	} catch (...) {
	}
#else
	(void)uri;
#endif

	f(value);
}

228 229 230
void
CurlInputStream::OnHeaders(unsigned status,
			   std::multimap<std::string, std::string> &&headers)
231
{
232
	assert(GetEventLoop().IsInside());
233
	assert(!postponed_exception);
234
	assert(!icy || !icy->IsDefined());
235

236
	if (status < 200 || status >= 300)
237 238 239
		throw HttpStatusError(status,
				      StringFormat<40>("got HTTP status %u",
						       status).c_str());
240

241
	const std::lock_guard<Mutex> protect(mutex);
242

243 244 245 246 247 248
	if (IsSeekPending()) {
		/* don't update metadata while seeking */
		SeekDone();
		return;
	}

249
	if (headers.find("accept-ranges") != headers.end())
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
		seekable = true;

	auto i = headers.find("content-length");
	if (i != headers.end())
		size = offset + ParseUint64(i->second.c_str());

	i = headers.find("content-type");
	if (i != headers.end())
		SetMimeType(std::move(i->second));

	i = headers.find("icy-name");
	if (i == headers.end()) {
		i = headers.find("ice-name");
		if (i == headers.end())
			i = headers.find("x-audiocast-name");
	}

	if (i != headers.end()) {
		TagBuilder tag_builder;
269 270 271 272 273 274

		WithConvertedTagValue(GetURI(), i->second.c_str(),
				      [&tag_builder](const char *value){
					      tag_builder.AddItem(TAG_NAME,
								  value);
				      });
275

276
		SetTag(tag_builder.CommitNew());
277
	}
278

279
	if (icy) {
280 281 282 283
		i = headers.find("icy-metaint");

		if (i != headers.end()) {
			size_t icy_metaint = ParseUint64(i->second.c_str());
284
			FmtDebug(curl_domain, "icy-metaint={}", icy_metaint);
285 286

			if (icy_metaint > 0) {
287
				icy->Start(icy_metaint);
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305

				/* a stream with icy-metadata is not
				   seekable */
				seekable = false;
			}
		}
	}

	SetReady();
}

void
CurlInputStream::OnData(ConstBuffer<void> data)
{
	assert(data.size > 0);

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

306 307
	if (IsSeekPending())
		SeekDone();
308 309 310

	if (data.size > GetBufferSpace()) {
		AsyncInputStream::Pause();
311
		throw CurlResponseHandler::Pause{};
312 313 314
	}

	AppendToBuffer(data.data, data.size);
315 316
}

317
void
318
CurlInputStream::OnEnd()
319
{
320
	const std::lock_guard<Mutex> protect(mutex);
321
	InvokeOnAvailable();
322 323 324 325 326

	AsyncInputStream::SetClosed();
}

void
327
CurlInputStream::OnError(std::exception_ptr e) noexcept
328
{
329
	const std::lock_guard<Mutex> protect(mutex);
330 331 332 333 334 335 336
	postponed_exception = std::move(e);

	if (IsSeekPending())
		SeekDone();
	else if (!IsReady())
		SetReady();
	else
337
		InvokeOnAvailable();
338

339
	AsyncInputStream::SetClosed();
340 341 342
}

/*
343
 * InputPlugin methods
344 345 346
 *
 */

347
static void
348
input_curl_init(EventLoop &event_loop, const ConfigBlock &block)
Max Kellermann's avatar
Max Kellermann committed
349
{
350 351
	try {
		curl_init = new CurlInit(event_loop);
352 353
	} catch (...) {
		std::throw_with_nested(PluginUnavailable("CURL initialization failed"));
354
	}
Max Kellermann's avatar
Max Kellermann committed
355

356 357
	const auto version_info = curl_version_info(CURLVERSION_FIRST);
	if (version_info != nullptr) {
358
		FmtDebug(curl_domain, "version {}", version_info->version);
359
		if (version_info->features & CURL_VERSION_SSL)
360 361
			FmtDebug(curl_domain, "with {}",
				 version_info->ssl_version);
362 363
	}

Max Kellermann's avatar
Max Kellermann committed
364
	http_200_aliases = curl_slist_append(http_200_aliases, "ICY 200 OK");
365

366
	proxy = block.GetBlockValue("proxy");
367
	proxy_port = block.GetBlockValue("proxy_port", 0U);
368 369
	proxy_user = block.GetBlockValue("proxy_user");
	proxy_password = block.GetBlockValue("proxy_password");
370

371 372 373 374 375 376
#ifdef ANDROID
	// TODO: figure out how to use Android's CA certificates and re-enable verify
	constexpr bool default_verify = false;
#else
	constexpr bool default_verify = true;
#endif
377
	cacert = block.GetBlockValue("cacert");
378 379
	verify_peer = block.GetBlockValue("verify_peer", default_verify);
	verify_host = block.GetBlockValue("verify_host", default_verify);
Max Kellermann's avatar
Max Kellermann committed
380 381
}

382
static void
383
input_curl_finish() noexcept
Max Kellermann's avatar
Max Kellermann committed
384
{
385
	delete curl_init;
386

Max Kellermann's avatar
Max Kellermann committed
387
	curl_slist_free_all(http_200_aliases);
388
	http_200_aliases = nullptr;
Max Kellermann's avatar
Max Kellermann committed
389 390
}

391
template<typename I>
392 393
inline
CurlInputStream::CurlInputStream(EventLoop &event_loop, const char *_url,
394
				 const std::multimap<std::string, std::string> &headers,
395
				 I &&_icy,
396 397
				 Mutex &_mutex)
	:AsyncInputStream(event_loop, _url, _mutex,
398 399
			  CURL_MAX_BUFFERED,
			  CURL_RESUME_AT),
400
	 icy(std::forward<I>(_icy))
401
{
402
	request_headers.Append("Icy-Metadata: 1");
403

404 405
	for (const auto &[key, header] : headers)
		request_headers.Append((key + ":" + header).c_str());
406 407
}

408
CurlInputStream::~CurlInputStream() noexcept
Max Kellermann's avatar
Max Kellermann committed
409
{
410
	FreeEasyIndirect();
Max Kellermann's avatar
Max Kellermann committed
411 412
}

413 414
void
CurlInputStream::InitEasy()
Max Kellermann's avatar
Max Kellermann committed
415
{
416
	request = new CurlRequest(**curl_init, GetURI(), *this);
417 418

	request->SetOption(CURLOPT_HTTP200ALIASES, http_200_aliases);
419 420 421
	request->SetOption(CURLOPT_FOLLOWLOCATION, 1L);
	request->SetOption(CURLOPT_MAXREDIRS, 5L);
	request->SetOption(CURLOPT_FAILONERROR, 1L);
Max Kellermann's avatar
Max Kellermann committed
422

423 424 425 426
	/* this option eliminates the probe request when
	   username/password are specified */
	request->SetOption(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);

427
	if (proxy != nullptr)
428
		request->SetOption(CURLOPT_PROXY, proxy);
429

430
	if (proxy_port > 0)
431
		request->SetOption(CURLOPT_PROXYPORT, (long)proxy_port);
432

433 434 435 436
	if (proxy_user != nullptr && proxy_password != nullptr)
		request->SetOption(CURLOPT_PROXYUSERPWD,
				   StringFormat<1024>("%s:%s", proxy_user,
						      proxy_password).c_str());
437

438 439
	if (cacert != nullptr)
		request->SetOption(CURLOPT_CAINFO, cacert);
440 441
	request->SetVerifyPeer(verify_peer);
	request->SetVerifyHost(verify_host);
442
	request->SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
443 444 445 446 447
}

void
CurlInputStream::StartRequest()
{
448
	request->Start();
Max Kellermann's avatar
Max Kellermann committed
449 450
}

451
void
452
CurlInputStream::SeekInternal(offset_type new_offset)
Max Kellermann's avatar
Max Kellermann committed
453 454 455
{
	/* close the old connection and open a new one */

456
	FreeEasy();
Max Kellermann's avatar
Max Kellermann committed
457

458
	offset = new_offset;
459
	if (offset == size) {
460 461 462
		/* seek to EOF: simulate empty result; avoid
		   triggering a "416 Requested Range Not Satisfiable"
		   response */
463
		SeekDone();
464
		return;
465
	}
466

467
	InitEasy();
Max Kellermann's avatar
Max Kellermann committed
468 469 470

	/* send the "Range" header */

471 472 473 474
	if (offset > 0)
		request->SetOption(CURLOPT_RANGE,
				   StringFormat<40>("%" PRIoffset "-",
						    offset).c_str());
475 476

	StartRequest();
477
}
478

479 480 481 482
void
CurlInputStream::DoSeek(offset_type new_offset)
{
	assert(IsReady());
483
	assert(seekable);
484 485 486

	const ScopeUnlock unlock(mutex);

487
	BlockingCall(GetEventLoop(), [this, new_offset](){
488 489
			SeekInternal(new_offset);
		});
Max Kellermann's avatar
Max Kellermann committed
490 491
}

492
inline InputStreamPtr
493 494
CurlInputStream::Open(const char *url,
		      const std::multimap<std::string, std::string> &headers,
495
		      Mutex &mutex)
Max Kellermann's avatar
Max Kellermann committed
496
{
497 498
	auto icy = std::make_shared<IcyMetaDataParser>();

499
	auto c = std::make_unique<CurlInputStream>((*curl_init)->GetEventLoop(),
500
						   url, headers,
501
						   icy,
502
						   mutex);
503

504 505 506 507
	BlockingCall(c->GetEventLoop(), [&c](){
			c->InitEasy();
			c->StartRequest();
		});
Max Kellermann's avatar
Max Kellermann committed
508

509
	return std::make_unique<MaybeBufferedInputStream>(std::make_unique<IcyInputStream>(std::move(c), std::move(icy)));
510 511
}

512 513 514
InputStreamPtr
OpenCurlInputStream(const char *uri,
		    const std::multimap<std::string, std::string> &headers,
515
		    Mutex &mutex)
516
{
517
	return CurlInputStream::Open(uri, headers, mutex);
518 519
}

520
static InputStreamPtr
521
input_curl_open(const char *url, Mutex &mutex)
522
{
523 524
	if (!StringStartsWithCaseASCII(url, "http://") &&
	    !StringStartsWithCaseASCII(url, "https://"))
525
		return nullptr;
526

527
	return CurlInputStream::Open(url, {}, mutex);
Max Kellermann's avatar
Max Kellermann committed
528
}
529

530
static std::set<std::string>
531 532
input_curl_protocols() noexcept
{
533 534 535 536 537 538 539 540 541 542 543
	std::set<std::string> protocols;
	auto version_info = curl_version_info(CURLVERSION_FIRST);
	for (auto proto_ptr = version_info->protocols; *proto_ptr != nullptr; proto_ptr++) {
		if (protocol_is_whitelisted(*proto_ptr)) {
			std::string schema(*proto_ptr);
			schema.append("://");
			protocols.emplace(schema);
		}
	}
	return protocols;
}
544

545
const struct InputPlugin input_plugin_curl = {
546
	"curl",
547
	nullptr,
548 549 550
	input_curl_init,
	input_curl_finish,
	input_curl_open,
551
	input_curl_protocols
552
};