From f11e5ff9cb3808b2a67c198aaf8882759a841297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Tue, 12 May 2026 14:15:27 +0200 Subject: [PATCH 1/6] stream_curl: tell libcurl to use the native CA store This is independent of other CA certificate locations set at run time or build time. Those locations are searched in addition to the native CA store. --- stream/stream_curl.c | 1 + 1 file changed, 1 insertion(+) diff --git a/stream/stream_curl.c b/stream/stream_curl.c index f0e8dcedb634a..da76afc60cf99 100644 --- a/stream/stream_curl.c +++ b/stream/stream_curl.c @@ -722,6 +722,7 @@ static void setup_curl(struct priv *p) if (p->net_opts->http_proxy && p->net_opts->http_proxy[0]) curl_easy_setopt(c, CURLOPT_PROXY, p->net_opts->http_proxy); + curl_easy_setopt(c, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NATIVE_CA); curl_easy_setopt(c, CURLOPT_SSL_VERIFYPEER, p->net_opts->tls_verify ? 1L : 0L); curl_easy_setopt(c, CURLOPT_SSL_VERIFYHOST, p->net_opts->tls_verify ? 2L : 0L); if (p->net_opts->tls_ca_file) { From a265d966a4d07cb03747eb9d68c79fdac78e3c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 11 May 2026 22:53:26 +0200 Subject: [PATCH 2/6] options: enable `tls-verify` by default We shouldn't default to no verification in current year. User can opt-out if needed. Keep it disable for builds without libcurl when libavformat is older than 63.0.100. This is done to match ffmpeg's timeline for enabling tls_verify by default in lavf, also to ensure we the default verify locations will be loaded on such version. --- DOCS/interface-changes/tls-verify-default.txt | 1 + DOCS/man/options.rst | 13 ++++++++++--- stream/network.c | 5 +++++ 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 DOCS/interface-changes/tls-verify-default.txt diff --git a/DOCS/interface-changes/tls-verify-default.txt b/DOCS/interface-changes/tls-verify-default.txt new file mode 100644 index 0000000000000..86251d555075e --- /dev/null +++ b/DOCS/interface-changes/tls-verify-default.txt @@ -0,0 +1 @@ +change `--tls-verify` default to `yes` diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index e2cf9f0714c76..e0dc6e4ba3942 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -5666,9 +5666,16 @@ Network Certificate authority database file for use with TLS. (Silently fails with older FFmpeg versions.) -``--tls-verify`` - Verify peer certificates when using TLS (e.g. with ``https://...``). - (Silently fails with older FFmpeg versions.) +``--tls-verify=`` + Verify peer certificates when using TLS (e.g. with ``https://...``) + (default: yes*). Disabling this option allows man-in-the-middle attacks + to silently substitute the content of an HTTPS stream and is only + recommended as a per-stream override when verification fails for a + known-good reason (e.g. an outdated CA bundle, a corporate proxy, a + development server with a self-signed certificate). + + This is disabled by default, if mpv is built without libcurl and + libavformat is older than 63.0.100. ``--tls-cert-file`` A file containing a certificate to use in the handshake with the diff --git a/stream/network.c b/stream/network.c index b2c7ce90f0fe7..95a0735ba3952 100644 --- a/stream/network.c +++ b/stream/network.c @@ -17,6 +17,8 @@ #include +#include + #include "network.h" #include "options/m_option.h" @@ -41,5 +43,8 @@ const struct m_sub_options mp_network_conf = { .defaults = &(const struct mp_network_opts){ .useragent = "libmpv", .timeout = 60, +#if HAVE_LIBCURL || LIBAVFORMAT_VERSION_MAJOR >= 63 + .tls_verify = true, +#endif }, }; From 639338ab26306baa14812f9e0d8226e243849d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 11 May 2026 22:55:02 +0200 Subject: [PATCH 3/6] stream_curl: log user friendly message when TLS verification fails --- stream/stream_curl.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/stream/stream_curl.c b/stream/stream_curl.c index da76afc60cf99..b466e271cef06 100644 --- a/stream/stream_curl.c +++ b/stream/stream_curl.c @@ -599,6 +599,20 @@ static void start_request(struct priv *p) curl_multi_add_handle(p->ctx->multi, p->curl); } +static void log_curl_error(struct priv *p, const char *what, CURLcode code) +{ + MP_ERR(p, "%s: %s\n", what, curl_easy_strerror(code)); + if (code == CURLE_PEER_FAILED_VERIFICATION || + code == CURLE_SSL_CACERT_BADFILE) + { + MP_ERR(p, + "TLS certificate verification failed.\n" + "This usually means an outdated CA bundle, a self-signed " + "certificate,\nor a MITM proxy on your network. To bypass at " + "your own risk, pass\n--tls-verify=no.\n"); + } +} + static void on_done(struct priv *p, CURLcode code) { bool aborted = atomic_load_explicit(&p->aborted, memory_order_relaxed); @@ -606,7 +620,7 @@ static void on_done(struct priv *p, CURLcode code) if (!p->probed) { // Connection died before any headers arrived. if (code != CURLE_OK && !aborted) - MP_ERR(p, "error: %s\n", curl_easy_strerror(code)); + log_curl_error(p, "error", code); mp_mutex_lock(&p->mtx); p->probed = true; mp_cond_broadcast(&p->cond); @@ -650,7 +664,7 @@ static void on_done(struct priv *p, CURLcode code) } if (!aborted) - MP_ERR(p, "transfer failed: %s\n", curl_easy_strerror(code)); + log_curl_error(p, "transfer failed", code); mp_mutex_lock(&p->mtx); p->stream_error = true; From 7e511e97bd67b69c01b1966462539ae22fd187e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 11 May 2026 22:36:16 +0200 Subject: [PATCH 4/6] stream/network: add Icecast metadata parser --- stream/network.c | 168 +++++++++++++++++++++++++++++++++++++++++++++++ stream/network.h | 40 +++++++++++ 2 files changed, 208 insertions(+) diff --git a/stream/network.c b/stream/network.c index 95a0735ba3952..76daba83ddda0 100644 --- a/stream/network.c +++ b/stream/network.c @@ -16,11 +16,19 @@ */ #include +#include #include +#include "common/common.h" +#include "common/tags.h" +#include "demux/demux.h" +#include "misc/charset_conv.h" +#include "mpv_talloc.h" #include "network.h" +#include "options/m_config.h" #include "options/m_option.h" +#include "stream.h" #define OPT_BASE_STRUCT struct mp_network_opts @@ -48,3 +56,163 @@ const struct m_sub_options mp_network_conf = { #endif }, }; + +struct mp_icy { + uint64_t metaint; // bytes between metadata blocks (0 = no ICY) + uint64_t data_read; // data bytes since last metadata block + enum { + ICY_DATA = 0, + ICY_LEN, + ICY_META, + } state; + size_t meta_pending; // bytes left in the current metadata block + size_t meta_pos; // bytes already accumulated in meta_buf + char meta_buf[255 * 16 + 1]; + bstr headers; // accumulated "Icy-Name: value\n" lines + bstr packet; // last metadata payload + bool dirty; // new metadata to deliver +}; + +struct mp_icy *mp_icy_new(void *ta_parent) +{ + return talloc_zero(ta_parent, struct mp_icy); +} + +void mp_icy_reset(struct mp_icy *i) +{ + i->metaint = 0; + i->data_read = 0; + i->state = ICY_DATA; + i->meta_pending = 0; + i->meta_pos = 0; + i->headers.len = 0; + i->packet.len = 0; + i->dirty = false; +} + +void mp_icy_add_header(struct mp_icy *i, bstr line) +{ + bstr name, val; + if (!bstr_split_tok(line, ": ", &name, &val)) + return; + if (!bstr_case_startswith(name, bstr0("Icy-"))) + return; + + if (bstrcasecmp0(name, "Icy-MetaInt") == 0) { + long long mi = bstrtoll(val, NULL, 10); + if (mi > 0) + i->metaint = mi; + } + // This may look a bit weird, that we join headers again, but it's done + // to share common parse function with lavf format later. + bstr_xappend_asprintf(i, &i->headers, "%.*s: %.*s\n", BSTR_P(name), BSTR_P(val)); + printf("ICY header: %.*s: %.*s\n", BSTR_P(name), BSTR_P(val)); + i->dirty = true; +} + +bool mp_icy_active(const struct mp_icy *i) +{ + return i->metaint > 0; +} + +void mp_icy_process(struct mp_icy *i, const char *buf, size_t len, + mp_icy_write_fn write_cb, void *ctx) +{ + if (!mp_icy_active(i)) { + if (len) + write_cb(ctx, buf, len); + return; + } + size_t pos = 0; + while (pos < len) { + switch (i->state) { + case ICY_DATA: { + size_t budget = i->metaint - i->data_read; + size_t take = MPMIN(len - pos, budget); + write_cb(ctx, buf + pos, take); + pos += take; + i->data_read += take; + if (i->data_read == i->metaint) + i->state = ICY_LEN; + break; + } + case ICY_LEN: { + uint8_t n = (uint8_t)buf[pos++]; + if (n == 0) { + i->state = ICY_DATA; + i->data_read = 0; + } else { + i->meta_pending = (size_t)n * 16; + i->meta_pos = 0; + i->state = ICY_META; + } + break; + } + case ICY_META: { + size_t take = MPMIN(len - pos, i->meta_pending); + memcpy(i->meta_buf + i->meta_pos, buf + pos, take); + i->meta_pos += take; + i->meta_pending -= take; + pos += take; + if (i->meta_pending == 0) { + i->meta_buf[i->meta_pos] = '\0'; + i->packet.len = 0; + bstr_xappend_asprintf(i, &i->packet, "%s\n", i->meta_buf); + i->dirty = true; + i->state = ICY_DATA; + i->data_read = 0; + } + break; + } + } + } +} + +struct mp_tags *mp_icy_get_metadata(struct mp_icy *i, struct stream *s) +{ + if (!i->dirty) + return NULL; + i->dirty = false; + return mp_parse_icy_metadata(s, i->headers, i->packet); +} + +struct mp_tags *mp_parse_icy_metadata(struct stream *s, bstr headers, bstr packet) +{ + if (!headers.len && !packet.len) + return NULL; + + struct mp_tags *res = talloc_zero(NULL, struct mp_tags); + + while (headers.len) { + bstr line = bstr_strip_linebreaks(bstr_getline(headers, &headers)); + bstr name, val; + if (bstr_split_tok(line, ": ", &name, &val)) + mp_tags_set_bstr(res, name, val); + } + + bstr head = bstr0("StreamTitle='"); + int i = bstr_find(packet, head); + if (i >= 0) { + packet = bstr_cut(packet, i + head.len); + int end = bstr_find(packet, bstr0("\';")); + if (end >= 0) + packet = bstr_splice(packet, 0, end); + + bool allocated = false; + struct demux_opts *opts = mp_get_config_group(NULL, s->global, &demux_conf); + const char *charset = mp_charset_guess(s, s->log, packet, opts->meta_cp, 0); + if (charset && !mp_charset_is_utf8(charset)) { + bstr conv = mp_iconv_to_utf8(s->log, packet, charset, 0); + if (conv.start && conv.start != packet.start) { + allocated = true; + packet = conv; + } + } + mp_tags_set_bstr(res, bstr0("icy-title"), packet); + talloc_free(opts); + if (allocated) + talloc_free(packet.start); + } + + return res; +} diff --git a/stream/network.h b/stream/network.h index d7516b2e43438..dd0498652c118 100644 --- a/stream/network.h +++ b/stream/network.h @@ -20,6 +20,9 @@ #include struct m_sub_options; +struct mp_tags; +struct stream; +typedef struct bstr bstr; struct mp_network_opts { bool cookies_enabled; @@ -36,3 +39,40 @@ struct mp_network_opts { }; extern const struct m_sub_options mp_network_conf; + +// Build mp_tags from accumulated ICY metadata. `headers` is a buffer of +// "Icy-*: value\n" lines collected from the response. `packet` is the most +// recent in-band metadata payload. Returns NULL when both are empty. +// Returned value has to be freed by the caller. +struct mp_tags *mp_parse_icy_metadata(struct stream *s, bstr headers, + bstr packet); + +// Opaque state for receiving ICY (Shoutcast/Icecast) metadata over an HTTP +// stream. This wrapper is not thread safe. +struct mp_icy; + +// Allocate a new ICY context as a talloc child of `ta_parent`. +struct mp_icy *mp_icy_new(void *ta_parent); + +// Reset all ICY state. Use between responses (e.g. across redirects). +void mp_icy_reset(struct mp_icy *icy); + +// Feed a single response header line (without trailing CRLF). Lines that +// don't start with "Icy-" are silently ignored. +void mp_icy_add_header(struct mp_icy *icy, bstr line); + +// True if Icy-MetaInt was seen and the body must be filtered. +bool mp_icy_active(const struct mp_icy *icy); + +// Body callback used by mp_icy_process(). Invoked once per contiguous +// stretch of non-metadata bytes. +typedef void (*mp_icy_write_fn)(void *ctx, const char *data, size_t len); + +// Process a body chunk. When ICY is active, metadata bytes are stripped and +// stashed internally, remaining data is delivered via `write_cb`. Otherwise +// the chunk is forwarded as a single call to `write_cb`. +void mp_icy_process(struct mp_icy *icy, const char *buf, size_t len, + mp_icy_write_fn write_cb, void *ctx); + +// Returns talloc-allocated mp_tags. +struct mp_tags *mp_icy_get_metadata(struct mp_icy *icy, struct stream *s); From 09506c7d2cc3cf0d6dd491d61ee6478ba40cf41c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 11 May 2026 22:37:44 +0200 Subject: [PATCH 5/6] stream_lavf: use common Icecast metadata parser --- stream/stream_lavf.c | 48 ++++---------------------------------------- 1 file changed, 4 insertions(+), 44 deletions(-) diff --git a/stream/stream_lavf.c b/stream/stream_lavf.c index 868f227cc73a0..7457a1b85fe77 100644 --- a/stream/stream_lavf.c +++ b/stream/stream_lavf.c @@ -22,10 +22,7 @@ #include "options/path.h" #include "common/common.h" #include "common/msg.h" -#include "common/tags.h" #include "common/av_common.h" -#include "demux/demux.h" -#include "misc/charset_conv.h" #include "misc/thread_tools.h" #include "stream.h" #include "network.h" @@ -456,51 +453,14 @@ static struct mp_tags *read_icy(stream_t *s) // Send a metadata update only 1. on start, and 2. on a new metadata packet. // To detect new packages, set the icy_metadata_packet to "-" once we've // read it (a bit hacky, but works). - struct mp_tags *res = NULL; - if ((!icy_header || !icy_header[0]) && (!icy_packet || !icy_packet[0])) - goto done; - bstr packet = bstr0(icy_packet); - if (bstr_equals0(packet, "-")) - goto done; - - res = talloc_zero(NULL, struct mp_tags); - - bstr header = bstr0(icy_header); - while (header.len) { - bstr line = bstr_strip_linebreaks(bstr_getline(header, &header)); - bstr name, val; - if (bstr_split_tok(line, ": ", &name, &val)) - mp_tags_set_bstr(res, name, val); - } - - bstr head = bstr0("StreamTitle='"); - int i = bstr_find(packet, head); - if (i >= 0) { - packet = bstr_cut(packet, i + head.len); - int end = bstr_find(packet, bstr0("\';")); - packet = bstr_splice(packet, 0, end); - - bool allocated = false; - struct demux_opts *opts = mp_get_config_group(NULL, s->global, &demux_conf); - const char *charset = mp_charset_guess(s, s->log, packet, opts->meta_cp, 0); - if (charset && !mp_charset_is_utf8(charset)) { - bstr conv = mp_iconv_to_utf8(s->log, packet, charset, 0); - if (conv.start && conv.start != packet.start) { - allocated = true; - packet = conv; - } - } - mp_tags_set_bstr(res, bstr0("icy-title"), packet); - talloc_free(opts); - if (allocated) - talloc_free(packet.start); - } + if (!bstr_equals0(packet, "-")) + res = mp_parse_icy_metadata(s, bstr0(icy_header), packet); - av_opt_set(avio, "icy_metadata_packet", "-", AV_OPT_SEARCH_CHILDREN); + if (res) + av_opt_set(avio, "icy_metadata_packet", "-", AV_OPT_SEARCH_CHILDREN); -done: av_free(icy_header); av_free(icy_packet); return res; From 874fcda49d407f1cfd11d9cd95ca0c20c426e932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 11 May 2026 22:39:00 +0200 Subject: [PATCH 6/6] stream_curl: add support for Icecast metadata --- stream/stream_curl.c | 60 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/stream/stream_curl.c b/stream/stream_curl.c index b466e271cef06..59924fb66c4a1 100644 --- a/stream/stream_curl.c +++ b/stream/stream_curl.c @@ -177,6 +177,7 @@ struct priv { bool stream_eof; // producer has delivered all data bool stream_error; // unrecoverable error atomic_bool aborted; // canceled by user (mp_cancel) + struct mp_icy *icy; // ICY metadata state, dormant until Icy-MetaInt seen }; // Curl thread @@ -346,6 +347,18 @@ static bool is_http_success(long resp) return resp >= 200 && resp < 300; } +// Append `len` bytes to the ring buffer. Caller must hold p->mtx and have +// verified that there is enough free space. +static void ring_write(void *ctx, const char *data, size_t len) +{ + struct priv *p = ctx; + size_t tail_chunk = MPMIN(p->buffer_size - p->tail, len); + memcpy(p->buffer + p->tail, data, tail_chunk); + memcpy(p->buffer, data + tail_chunk, len - tail_chunk); + p->tail = (p->tail + len) % p->buffer_size; + p->count += len; +} + // Called per chunk of body data. static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { @@ -370,13 +383,9 @@ static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdat return CURL_WRITEFUNC_PAUSE; } - size_t tail_chunk = MPMIN(p->buffer_size - p->tail, bytes); - memcpy(p->buffer + p->tail, ptr, tail_chunk); - memcpy(p->buffer, ptr + tail_chunk, bytes - tail_chunk); - p->tail = (p->tail + bytes) % p->buffer_size; - p->count += bytes; - p->paused = false; + mp_icy_process(p->icy, ptr, bytes, ring_write, p); + p->paused = false; p->request_received += bytes; mp_cond_broadcast(&p->cond); @@ -434,8 +443,18 @@ static void finalize_probe(struct priv *p) // we care about the final one. static void probe_http(struct priv *p, struct bstr line) { - if (line.len > 0) + if (line.len > 0) { + // A new status line resets per-response state so that intermediate + // 1xx/3xx responses don't leak ICY metadata into the final one. + mp_mutex_lock(&p->mtx); + if (bstr_startswith0(line, "HTTP/")) { + mp_icy_reset(p->icy); + } else { + mp_icy_add_header(p->icy, line); + } + mp_mutex_unlock(&p->mtx); return; + } long resp = 0; curl_easy_getinfo(p->curl, CURLINFO_RESPONSE_CODE, &resp); @@ -455,8 +474,10 @@ static void probe_http(struct priv *p, struct bstr line) bool accept_ranges = ar && strcasecmp(ar, "bytes") == 0; // Some servers reply 200 to an open-ended "Range: 0-" but 206 to explicit - // byte ranges, so trust either. - p->seekable = !compressed && (resp == 206 || accept_ranges); + // byte ranges, so trust either. ICY metadata is interleaved with the body, + // so byte ranges from the server don't line up with consumer offsets. + p->seekable = !compressed && !mp_icy_active(p->icy) && + (resp == 206 || accept_ranges); if (p->seekable) { // Content-Range carries the full size on a partial response. On any @@ -697,6 +718,8 @@ static struct curl_slist *build_header_list(struct priv *p) for (int i = 0; p->net_opts->http_header_fields[i]; i++) list = curl_slist_append(list, p->net_opts->http_header_fields[i]); } + if (p->scheme->proto == MP_CURL_PROTO_HTTP) + list = curl_slist_append(list, "Icy-MetaData: 1"); return list; } @@ -829,6 +852,23 @@ static int64_t curl_get_size(struct stream *s) return p->content_size; } +static int curl_control(struct stream *s, int cmd, void *arg) +{ + struct priv *p = s->priv; + switch (cmd) { + case STREAM_CTRL_GET_METADATA: { + mp_mutex_lock(&p->mtx); + struct mp_tags *tags = mp_icy_get_metadata(p->icy, s); + mp_mutex_unlock(&p->mtx); + if (!tags) + break; + *(struct mp_tags **)arg = tags; + return STREAM_OK; + } + } + return STREAM_UNSUPPORTED; +} + static void priv_destructor(void *ptr) { struct priv *p = ptr; @@ -886,6 +926,7 @@ static int curl_open(stream_t *s, const struct stream_open_args *args) p->content_size = -1; p->buffer_size = p->opts->buffer_size; p->buffer = talloc_size(p, p->buffer_size); + p->icy = mp_icy_new(p); mp_mutex_init(&p->mtx); mp_cond_init(&p->cond); @@ -922,6 +963,7 @@ static int curl_open(stream_t *s, const struct stream_open_args *args) s->fill_buffer = curl_fill_buffer; s->seek = p->seekable ? curl_seek : NULL; s->get_size = curl_get_size; + s->control = curl_control; s->close = curl_close; return STREAM_OK;