Тема: Алгоритм подписи youtube
Спрашивали - отвечаю.
При загрузке страницы с youtube, если видео доступно для воспроизведения, в коде html содержится json данные, под названием ytplayer.config. Там содержится вся информация о видео, его параметры и, самое главное, ссылки на видео и аудио потоки (файлы).
Форматы этих файлов разнообразны, отличаются кодеком, качеством и проч.
Ссылки на эти файлы содержат миллиарды кучу параметров. В том числе такой параметр как sig (от слова signature - подпись).
В тех видео, в которых есть какие-либо материалы так называемых партнёров (стоит какой-нибудь копирайт, например на музыку), там этот параметр sig отсутствует. Точнее даже отсутствует прямо указанный url.
Но вместо него в json данных о формате медиа присутствует поле "cipher", в котором в формате urlEncode (через &) перечислены три параметра:
- url - ссылка на медиа-поток, но без параметра sig.
- s - заготовка для подписи (какбы значение подписи, но не валидная). Вот её то и нужно дешифровать.
- sp - имя параметра подписи. Обычно всегда "sig".
Т.е. ссылка на поток будет равна полученной url плюс разделитель параметров "&", плюс имя параметра указанного в "sp", плюс знак равно, плюс дешифрованное значение "s".
Осталось дешифровать этот s.
Сами функции дешифровки находятся в js-скрипте, путь до которого можно взять из html кода или из ytplayer.config.assets.js. Выглядит обычно как /yts/jsbin/player_ias-vfl2ChXMk/ru_RU/base.js
, где vfl2ChXMk
- некий идентификатор плеера, который постоянно меняется со временем и может зависеть от региона и проч. Вместе с ним меняется и алгоритм дешифровки подписи.
Скриптик такой не хилый, на 1,2 метра.
Обычно, на сегодняшний день, функция дешифровки (её имя меняется от версии к версии) всё равно имеет примерно один и тот же вид. Но меняются местами вызовы функций и их параметры.
В js-скрипте плеера я ищу эту функцию по ключевым словам: a=a.split("")
, она уже много лет так начинается. Например, в вышеприведённом js-скрипте эта функция такая:
Os=function(a){a=a.split("");Ns.yf(a,1);Ns.Wb(a,36);Ns.hi(a,64);return a.join("")};
Ищут её по регуляркам. Например, в скрипте на php она сейчас такая:
$fns = preg_match('/\b\w{2}=function\(a\)\{a=a\.split\(""\);(.*?)return/s', $data, $m) ? $m[1] : ''; // Шаблон поиска функции дешифровки
Можно заметить, что в самой функции вызываются по очереди ещё функции, с переданными параметрами: Ns.yf(a,1);
, Ns.Wb(a,36)
, Ns.hi(a,64);
. Параметр a - это и есть значение подписи, которое дешифруется. Эти функции также нужно найти в этом же скрипте и посмотреть, что они из себя представляют.
Их также можно найти по регуляркам. Т.е. нужно найти, где они в js-скрипте объявляются.
Например, объявление объекта Ns выглядит следующим образом:
var Ns={Wb:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c},hi:function(a){a.reverse()},yf:function(a,b){a.splice(0,b)}};
Немножко отформатируем:
var Ns={
Wb:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c},
hi:function(a){a.reverse()},
yf:function(a,b){a.splice(0,b)}
};
У объекта Ns первая функция Wb:
Wb:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}
Если приглядеться, смысл этой функции сводится к тому, что меняются местами два символа, первый и по указанному индексу в параметрах.
Функция Ns.hi - переворачивает строку.
Функция Ns.yf - разбивает (по сути обрезает) строку с указанного символа.
Народ заметил, что названия этих функций и объекта их содержащий постоянно меняются, также меняются числовые значения параметров от версии к версии js-скрипта. Но суть всегда остаётся (уже много лет): в разной последовательности, в разном количестве и с разными параметрами, но функции преобразования всегда три типа. Уж не знаю кто первый додумался, но остальные подсмотрели и подхватили - все три функции можно описать буквами, а значения параметров можно указать рядом с буквой:
r - revers, s - slice, w - swap
И функцию дешифровки Os=function(a){a=a.split("");Ns.yf(a,1);Ns.Wb(a,36);Ns.hi(a,64);return a.join("")};
можно записать как s1 w36 r
и по этому алгоритму расшифровать. А ещё лучше, именно для такого идентификатора js-скрипта кэшировать эти значения, дабы не качать постоянно этот тяжелый js.
Т.е. для дешифровщика эта строка s1 w36 r
будет означать, что нужно со строкой s (заготовкой из поля "cipher", про которую говорилось в начале) сделать следующее:
- s1 - взять строку с символа 1 (индекс начинается с 0)
- w36 - поменять местами символ 0 и 36
- r - перевернуть строку задом наперёд
Самое сложное и в то же время ненадёжное во всём этом - это поиск по регуляркам. Потому как гугл, нет нет, да что-то меняет. Придумывает как сменить вид этих функций. Поэтому приходится иногда подправлять эти регулярки.
Именно этим - получением алгоритма - занимается функция GetAlgorithm на php в файле getalgo.php:
///////////////////////////////////////////////////////////////////////////////
// Функция поиска алгоритма дешифровки подписи по ссылке на js-скрипт
function GetAlgorithm($jsUrl) {
if (substr($jsUrl, 0, 2)=="//") $jsUrl = "https:".$jsUrl;
else if (substr($jsUrl, 0, 1)=="/" ) $jsUrl = "https://www.youtube.com".$jsUrl;
$algo = "";
$data = file_get_contents($jsUrl);
$fns = preg_match('/\b\w{2}=function\(a\)\{a=a\.split\(""\);(.*?)return/s', $data, $m) ? $m[1] : ''; // Шаблон поиска функции дешифровки
$arr = explode(';', $fns); // Получаем массив вызываемых команд в полученной функции дешифровки
// Перебираем все вызовы в полученной функции дешифровки из js-скрипта
foreach ($arr as $func) {
$textFunc = $func; // Текст вызываемой команды или функции
// Если вызывается конкретная функция объекта - ищем объявление этого объекта и его функции в js-скрипте
if (preg_match('/([\$\w]+)\.(\w+)\(/s', $textFunc, $m)) {
$obj = $m[1]; // Имя объекта
$fun = $m[2]; // Имя его вызываемой функции
// Попытка найти объявление объекта и его функции по шаблону
if (($obj!='a') && preg_match('/var '.$obj.'=\{.*?('.$fun.':function|function '.$fun.'\()(.*?})/s', $data, $m))
$textFunc = $m[2]; // Если нашли - перезаписываем текст вызова дешифровки этой итерации
else if (($obj!='a') && preg_match('/var \\'.$obj.'=\{.*?('.$fun.':function|function '.$fun.'\()(.*?})/s', $data, $m))
$textFunc = $m[2]; // Если нашли - перезаписываем текст вызова дешифровки этой итерации
}
// Если вызывается именованная функция - поиск текста этой функции в js-скрипте
if (preg_match('/a=(\w+)\(/s', $textFunc, $m)) {
$fun = $m[1]; // Имя вызываемой функции
// Попытки найти объявление этой функции по полученному имени в тексте js-скрипта
if (preg_match('/var '.$obj.'=\{.*?('.$fun.':function|function '.$fun.'\())(.*?})/s', $data, $m))
$textFunc = $m[2]; // Если нашли - перезаписываем текст вызова дешифровки этой итерации
else if (preg_match('/var \\'.$obj.'=\{.*?('.$fun.':function|function '.$fun.'\())(.*?})/s', $data, $m))
$textFunc = $m[2]; // Если нашли - перезаписываем текст вызова дешифровки этой итерации
}
// Получаем значение параметра в вызываемой команде или функции
$numb = preg_match('/\(.*?(\d+)/s', $func, $m) ? $m[1] : '';
// Определяем тип вызываемой функции в данной итерации
$type = 'w'; // По-умолчанию w = Swap - поменять местами первый символ с символом по указанному индексу
if (preg_match('/revers/' , $textFunc, $m)) $type = 'r'; // 'r' = Revers - перевернуть строку задом наперёд
elseif (preg_match('/(splice|slice)/', $textFunc, $m)) $type = 's'; // 's' = Slice - обрезать строку на указанную длину
if (($type!='r') && ($numb==='')) continue; // Если нет параметра у функции и это не Revers, то пропускаем, это не команда дешифровки
$algo .= ($type=='r') ? $type.' ' : $type.$numb.' '; // Формируем алгоритм, указывая тип и значение параметра в виде "w4 r s29"
}
return trim($algo);
}
Эта функция возвращает строку типа w23 s2 r w8
. А вот такая функция дешифрует подпись указанным алгоритмом (из файла g.php):
///////////////////////////////////////////////////////////////////////////////
// Функция дешифровки заготовки подписи по указанному алгоритму в виде строки "w12 s34 r w9"
function YoutubeDecrypt($sig, $algorithm) {
$method = explode(" ", $algorithm); // Получаем массив команд дешифровки
if (!$sig) return ""; // Если нет заготовки подписи, то и дешифровать нечего
foreach($method as $m)
{ // Первая буква команды - тип: r - revers, s - slice, w - swap
// Вторая буква команды - значение параметра вызываемой команды
if ($m =='r') $sig = strrev($sig);
elseif(substr($m,0,1)=='s') $sig = substr($sig, (int)substr($m, 1));
elseif(substr($m,0,1)=='w') $sig = swap($sig, (int)substr($m, 1));
}
return $sig;
}
///////////////////////////////////////////////////////////////////////////////
// Поменять местами первый символ в строке с символом по указанному индексу
function swap($str, $b) {
$c = $str[0]; $str[0] = $str[$b]; $str[$b] = $c;
return $str;
}
Вот такой вот принцип подписи видео на youtube. Надеюсь был полезен и теперь эти скрипты можно самим дорабатывать, если они перестают в какой-то момент работать.
Я обновил скрипт g.php на гитхабе и некоторые могли заметить, что я с каких то времён убрал со своего сервера этот опубликованный скрипт. Это сделал потому, что лично мне этот скрипт не нужен, а запросов к нему было столько, что youtube заблокировал получение данных с IP моего сервера и отвечал тем, что уж слишком много от меня идёт к нему запросов. В общем, блочит.
Поэтому, выкладывайте этот скрипт каждый у себя, а публиковать для всеобщего пользования на своём серваке я пока не буду.
Хотя, для проверки работоспособности и отладки, я всё-таки написал страничку с js-кодом, который этот скрипт проверяет и выводит информацию о доступных видео потоках. Что-то вроде savefrom.net только для одного youtub-а.
Youtube get links
Кстати да, с прошлого раза g.php теперь поменял формат отдачи, точнее структуру json ответа и ограничил количество параметров. Теперь он просто всегда отдаёт объект, содержащий о всех доступных ссылках видео. В массиве formats - ссылки на видео со звуком (их обычно одна или две), в массиве adaptiveFormats - ссылки на видео без звука и аудио, всё как отдаёт сам youtube. Единственное, скрипт на лету их, если нужно, декодирует.