0
<< предыдущая заметкаследующая заметка >>
15 февраля 2022
загадки m3u8 или как скачать песню с Вконтактика

Спросили меня знакомые, могу ли я скачать какую-то песню с ВКонтактика. Я ответил, что не вижу проблемы - что доступно браузеру, то доступно и пользователю. Но оказалось дело немного хитрее.

Сразу скажу: не надо советов, каким приложением или программой воспользоваться, у меня нет нужды качать что-либо с ВК и вообще не интересны практические задачи. Интересен сам принцип внутреннего устройства технологии, который нужен для понимания сути процессов или для каких-то моих будущих проектов.

Принцип нынешнего устройства ВК оказался таким. Рассмотрим на песне Гребенщикова, которая во ВК после клика представляет собой некий файл вида: https://*.vkuseraudio.net/***/index.m3u8 Точный адрес файла m3u8 без особого труда выясняется в отладчике браузера. В нем перечислена песня, разбитая на кусочки вида:

#EXTINF:20.000,
seg-5-a1.ts
#EXTINF:20.000,
seg-6-a1.ts

Сам m3u8 и кусочки ts лежат на сервере в одной папке - имеют единый базовый урл, не привязаны к авторизации и кукам и отдаются совершенно спокойно. Проблема в том, что хитрый ВК некоторую часть кусочков шифрует протоколом AES. И только часть - чтобы теоретически юный хакер мог скачать фонограмму, собрав ее из кэша браузера или вытянув по кусочкам, и песня у него в принципе играла, но с потерей около 30% аудио в разных местах. Достигается это переключением на время перед некоторыми кусками в режим шифрования - тэг EXT-X-KEY с инструкцией шифрования и ключом key.pub. Ключ один и тот же для всех пакованных кусков фонограммы, лежит в той же папке.

#EXT-X-KEY:METHOD=AES-128,URI="https://.../key.pub"
#EXTINF:20.000,
seg-19-a1.ts
#EXT-X-KEY:METHOD=NONE

Чисто теоретически распаковку ts можно сделать самостоятельно - через openssl, указав ключ и некое IV (засев). Которое взять непонятно где, потому что в принципе по формату оно должно указываться в той же строке:
#EXT-X-KEY:METHOD=AES-128,URI="https://key.pub",IV=0x000000000000000000000000000000AE
Если же IV не указано, формат предписывает брать «attribute value or the Media Sequence Number as the IV», то есть, как я понял, тупо порядковый номер секции. В принципе на практике это работает, если заранее скачать его и перекодировать файл ключа key.pub, а IV указать равным 0 для любого пакованного участка:

openssl aes-128-cbc -d -K `xxd -p key.pub` -iv 0 -nosalt -in seg-4-a1.ts -out out.ts
mplayer ./out.ts

Но на практике многие плееры умеют такой файл воспроизводить сами, например, ссылку можно подсунуть VLC, и она заиграет: vlc https://.../index.m3u8

Но программы, способные не только играть, но и скачать файл, имеют глюк. Как минимум - построенные на базе ffmpeg. Это какой-то системный глюк ffmpeg, который бы разработчикам неплохо у себя пофиксить. Начнем с того, что ffplay https://index.m3u8 - не сработает, будет играть битый файл. То же самое произойдет и с попыткой забрать https://index.m3u8 через ffmpeg. Кстати, формат ключей ffmpeg - тоже отдельная пляска с бубном, для аудио (не видео) ключи должны быть такими:

ffmpeg -report -protocol_whitelist file,tls,tcp,https,http,crypto -allowed_extensions ALL -i 'https://.../index.m3u8' -c copy file.mp3

Симптомы те же: ffmpeg охотно бегает на далекий сайт за index.m3u8, за кусочками seg-%%-a1.ts и даже всякий раз за pub.key, но правильно распаковать шифрованные куски не может, поэтому просто их пропускает.

Что симптоматично: если добавлять ",IV=...", он вообще никак не реагирует. В том числе - если дописывать белиберду в исправленный файл. Исправить файл, как оказалось, можно лишь одним способом: скачав все шифрованные куски себе на локальный диск и изменив их адреса в index.m3u8:

#EXT-X-KEY:METHOD=AES-128,URI="https://.../key.pub"
#EXTINF:20.000,
/tmp/ts/seg-19-a1.ts

Только в этом случае ffmpeg расшифрует пакованные куски как надо, хотя за key.pub и нешифрованными кусками продолжит бегать на далекий сайт.

Почему это происходит - загадка. Причем, на дописанные IV= он не будет обращать никакого внимания, любое значение там укажи, а кусок с локального диска все равно распакуется правильно. Если же кусок лежит в сети, то он распакуется правильно только если по счету первый, проблемы начинаются лишь с распаковкой кусков, которые не первые. Из чего мы может предположить, что это действительно проблема IV (Initialization Value), которое ffmpeg неправильно вычисляет, если за куском ему приходится обращаться в сеть, а не на локальный диск. А может, наоборот, вычисляет правильно по стандарту, в то время, как хитрый VK пакует все части с IV=0, но использует какие-то глюки остального софта, которые не соблюдают стандарт и берут IV в случае его отсутствия равным номеру секции. Так или иначе, этот глюк с IV эксплуатирует нынешний VK чтобы затруднить "кражу" музыки, которую он по какой-то загадочной причине считает своей собственностью.

Поэтому я для себя нашел самый простой способ утянуть песенку из ВК при помощи ffmpeg - это выяснить её урл вида https://.../index.m3u8 и натравить на него ffmpeg, предварительно изменив в файле некоторые строки и скачав себе на локальный диск только те куски, что обозначены зашифрованными:

показать vkload.php
#!/usr/bin/php

<?php
$D
='/tmp/ts/'// папка для временных файлов
$F=$D."out.m3u8"// временный файл m3u8

if ( $argc || in_array($argv[1], array('--help''-help''-h''-?')) )
die(
'use: vkload.php "https://cs9-9v4.vkuseraudio.net/s/v1/ac/9nhZ1e3cvDRD8ic4K51DoGA_mYloGu3XkcQpGZ1nMdhel3fXU-6knE0FBZt2bU6GrPxgdEzl5kJU33U5oxd5Y9Msg0b2_qKAc9tymoCQa7fI57LlEitTnEiixfVygeaofYQH4qFyvm85I_-WtxZ7MUtYnduOpgWAn9n9hvK3vH7nJIs/index.m3u8"');

$out=md5($argv[1]).".mp3"// создадим уникальное имя для конечного файла
if(is_file($out)) die("File exist: ".$out);

$s=file_get_contents($argv[1]); // скачаем file.m3u8

if( preg_match("/\#EXT\-X\-KEY\:METHOD=AES\-128\,URI\=\"([^\"]+)\"\n/s",$s,$x) ) $url=dirname($x[1])."/";
preg_match_all("/.+?(\.ts\n|$)/s",$s,$m);

if(!
is_dir($D)) { mkdir($D); chmod($D,0777); } else { $r=glob($D."*.ts"); foreach($r as $lunlink($l); } // подготовим временную папку

$r=array(); foreach($m[0] as $n=>$p) {

    
$dd = ( preg_match("/\#EXT\-X\-KEY\:METHOD=AES\-128\,URI\=\"([^\"]+)\"\n/s",$p,$x) ? $D $url); // источник файла
    
if( preg_match("/[^\s]+\.ts\n/s",$p,$x) ) {
    
$name=trim($x[0]);
    
$p=str_replace($name,$dd.$name,$p);
    if(
$dd==$D && !is_file($D.$name)) { echo "\npreload: ".$D.$namefile_put_contents($D.$name,file_get_contents($url.$name)); }
    }
    
$r[]=$p;
}

file_put_contents($F,implode('',$r));
exec("ffmpeg -report -protocol_whitelist file,tls,tcp,https,http,crypto -allowed_extensions ALL -i ".$F." -c copy ".$out);

if(
is_dir($D)) { $r=glob($D."*"); foreach($r as $lunlink($l); rmdir($D); } // чистим мусор за собой

die("DONE: ".$out);
?>
<< предыдущая заметка следующая заметка >>
пожаловаться на эту публикацию администрации портала
архив понравившихся мне ссылок

Комментарии к этой заметке скрываются - они будут видны только вам и мне.

Оставить комментарий