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..76daba83ddda0 100644 --- a/stream/network.c +++ b/stream/network.c @@ -16,9 +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 @@ -41,5 +51,168 @@ 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 }, }; + +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); diff --git a/stream/stream_curl.c b/stream/stream_curl.c index f0e8dcedb634a..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 @@ -599,6 +620,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 +641,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 +685,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; @@ -683,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; } @@ -722,6 +759,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) { @@ -814,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; @@ -871,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); @@ -907,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; 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;