среда, 6 июня 2018 г.

Пишем JSFuck-транслятор

  1. О чем пойдет речь
  2. Базовые понятия
  3. Приступим к написанию кода
  4. Дополним базовый словарь
  5. Работа с функциями
  6. Использование encodeURI вместо escape
  7. Несколько мыслей по поводу оптимизации вывода
  8. Оптимизация работы с fromCharCode
  9. Немного тестов

О чем пойдет речь



Для начала цитата из Википедии
Цитата:
JSFuck is an esoteric programming style of JavaScript, where code is written using only six characters: [,], (, ), !, and +. The name is derived from Brainfuck, an esoteric programming language which also uses a minimalistic alphabet of only punctuation. Unlike Brainfuck, which requires its own compiler or interpreter, JSFuck is valid JavaScript code, meaning JSFuck programs can be run in any web browser or engine that interprets JavaScript.
Цитата:
JSFuck - эзотерический программный стиль языка JavaScript, код которого написан всего шестью символами: [,], (, ), !, и +.
Имя произведено от Brainfuck, эзотерического языка программирования, который также использует минималистический алфавит, состоящий исключительно из знаков пунктуации. В отличие от Brainfuck, для которого требуется собственный компилятор или интерпретатор, JSFuck является валидным кодом JavaScript, что подразумевает, что JSFuck-программа может быть запущена в каком-либо браузере или движке, который интерпретирует JavaScript.
JSFuck - Wikipedia
Существует ресурс JSFuck - Write any JavaScript with 6 Characters: []()!+, где можно перекодировать обычный JavaScript в JSFuck и запустить его либо там же либо в браузере. Есть также на гитхабе полный листинг готового транслятора.

Отсюда возникает вопрос: зачем я все это пишу, если все это уже есть в готовом виде и скорей всего оно даже лучше того, что в конце концов получится у меня? А пишу я это потому, что сам хочу разобраться в вопросе. Когда я впервые увидел код JSFuck, запустил его в браузере и увидел, что это работает, я не поверил своим глазам. То есть мне было понятно, что можно что-то построить, что будет возвращать текст программы, но чтобы его запустить, надо какие-то ключевые слова писать или хотя бы eval вызвать или что-то подобное. Но нет же: код, состоящий исключительно из символов []()+! работал без каких-либо дополнительных костылей. Поэтому я решил, что чтобы понять, как это все происходит, надо самому написать транслятор JavaScript-to-JSFuck. Я не ставлю цели написать что-то оптимальное по любому параметру, главное - это чтобы с его помощью можно было перекодировать JavaScript в JSFuck и чтобы это работало в браузере.

Базовые понятия



Для начала разберемся, откуда вообще в этом нагромождении одних и тех же символов может взяться что-то вразумительное типа букв или цифр. Секрет в том, что язык JavaScript - динамически типизированый и если выполняется какая-то операция над объектами, которая для объектов этих типов не предусмотрена, то движок языка пытается хоть как-нибудь это дело интерпретировать. В результате иногда выдает что-то, что мы можем использовать для решения нашей задачи.

Во-первых, рассмотрим символы, которые есть в наличии, и посмотрим, что с ними можно сделать.
[] - квадратные скобки могут использоваться как литерал массива и таким образом мы можем, просто написав их, создать экземпляр массива. Кроме того они могут использоваться для обращения к методам и свойствам объектов, а также индексам массивов. Возможность обращаться к методам и свойствам здесь критически важна, поскольку точки у нас нет, да и не помогла бы она, поскольку нет таких имен в JavaScript, которые состояли бы из имеющихся у нас символов.

() - с круглыми скобками все более-менее понятно, поскольку весь код у нас будет одним большим выражением, то группировка различных его частей - критически важна.

! - логичское НЕ, но не только. Поскольку в JavaScript, как уже упоминалось выше, движок все пытается как-то интерпретировать, а данный оператор применяется только к типу Boolean, то любое выражение, к которому будет применен этот оператор, движок будет конвертировать в Boolean, то есть это еще и конвертер типов.

+ - данный оператор помимо сложения чисел и конкатенации строк, также может использоваться как конвертер типов. Унарный плюс конвертирует операнд к типу Number, а бинарный нередко может использоваться как конвертер к типу String.

Теперь посмотрим, что можно сделать. Поскольку единственный объект, который можем создать на данном этапе - это массив, то с него и начнем.

[] - это просто массив. А если сложить два массива []+[], то получится пустая строка. Происходит этот потому, что для массива не определен оператор +, поэтому движок конвертирует оба объекта в строку и складывает строки. Почему строка оказывается пустой? Ну вот взять перевести в строку следующий массив [1, 2, 3].toString() и получим строку 1, 2, 3, то есть элементы массива будут выписаны через запятую без скобок. А что выдаст пустой массив? То же самое, только это и будет пустая строка. А две пустых строки при конкатенации дают пустую строку.

Теперь попробуем применить к массиву операторы.
![] - false
+[] - 0
А если к false применить !, то получится true
!![] - true
Мы уже выяснили, что если мы используем оператор + для объектов, для типов которых он не определен, то он сначала будет превращать все в строки. А стало быть
[]+![] - "false"
[]+!![] - "true"
Кроме того, если запросить несуществующий ключ объекта любого типа, то мы получим undefined. Ну например
[][[]] - undefined
Само по себе это мало что дает, а вот вот это пригодится
[]+[][[]] - "undefined"
Далее числа
+[] - 0
+!+[] - 1
+!+[]+!+[] - 2
+!+[]+!+[]+!+[] - 3

Теперь, имея в арсенале базовый набор слов и чисел, мы можем получать символы и составлять из них слова.
"false"[0] - "f"
"true"[1]+"true"[2]+"false"[2]+"true"[3] - "rule"
Или можно так
([]+!![])[+!+[]]+([]+!![])[+!+[]+!+[]]+([]+![])[+!+[]+!+[]]+([]+!![])[+!+[]+!+[]+!+[]] - "rule"

Таким образом мы составили из нашего скудного алфавита простое слово и можем двигаться дальше.

Приступим к написанию кода



Создадим класс JSFuck и в конструкторе определим базовый словарь, состоящий из слов, которые можно получить построив простые выражения. На данный момент у нас есть три таких слова true, false и undefined.
Код javascript Выделить
    class JSFuck
    {
        constructor()
        {
            this.basics = {
                "true": "([]+!![])",
                "false": "([]+![])",
                "undefined": "([]+[][[]])"
            }
        }
    }
Учитывая, что нам понадобится индексировать слова, приступим к созданию методов, с помощью которых можно будет генерировать коды чисел. Создадим два метода: getDigit - для получения кода цифры, и getInteger - для получения кода целого числа. Выше я уже показывал, что числа можно получать путем сложения единиц, то есть +!+[] - единица, можно повторять это несколько раз и единицы будут склаываться. Но для генерации больших чисел это не очень удобно. Поэтому лучше опсанным способом создавать отдельные цифры, цифры преобразовывать в строки, строки складывать и, при неободимости, опять переводить в числа. То есть для создания числа 123 не нужно складывать единицу 123 раза. вместо этого мы возьмем единицу, отдельно, две единицы отдельно и т. д. То есть +("1"+"2"+"3"). Таким образом цифры мы будем генерировать хоть и в числовом формате, но при этом будем упаковывать их в массивы. Потому что при выполнении операции [1]+[2]+[3] мы получим строку "123". Таким образом функция getDigit будет выглядеть так
Код javascript Выделить
            getDigit(d)
            {
                d = +d;
                return "[" + (d == 0 ? "+[]" : Array.apply(null, { length: d }).fill("+!+[]").join("")) + "]";
            }
Она принимает число, а возвращает строку, представляющую массив с нужным количеством единиц или ноль, например получает 3, а возвращает "[+!+[]+!+[]+!+[]]". Теперь из цифр можно строить числа
Код javascript Выделить
            getInteger(n)
            {
                return n.toString().split("").map((v) => this.getDigit(+v)).join("+");
            }
Этот метод принимает целое неотрицательное число и строит из него код, складывая цифры, как описанов выше.

Имея базовый словарь и средство построения кода чисел, мы можем написать метод encodeText, который будет строить слова из букв, входящий в слова, содержащиеся в словаре.
Начнем кодировать текст.
Код javascript Выделить
        encodeText(text)
        {
            let digits = "0123456789";
            let allChars = Object.keys(this.basics).join("").split("").filter((v, i, a) => a.indexOf(v) == i);
            let result = [];
            for (let char of text)
            {
                if (allChars.includes(char))
                {
                    for (let w in this.basics)
                    {
                        let index = w.split("").findIndex(c => c == char);
                        if (index > -1)
                        {
                            result.push(`(${"([]+" + this.basics[w] + ")"}[${this.getInteger(index)}])`);
                            break;
                        }
                    }
                }
                else if (digits.indexOf(char) > -1)
                {
                    result.push(this.getDigit(char));
                }
                else
                {
                    return "";
                }
            }
            return result.join("+");
        }
Здесь мы обходим текст посимвольно и проверяем сначала его наличие в словаре: если проверка удачна, то находим в словаре слово с этим символом, и формируем код, который добавляется в массив с результатом. Если символ не прошел проверку, то проверям, не является ли он цифрой, поскольку для них у нас тоже есть метод кодирования. Если ни одна проверка не прошла, то пока будем завершать функцию с возвращением пустой строки. Когда мы создадим другие инструменты, мы допишем дополнительную логику.

Дополним базовый словарь



На данный момент уже можно поэкспериментировать с полученным классом и сгенерировать те слова, которые можно составить из имеющихся в словаре символов.
Понятно, что с теми буквами, которые есть у нас сейчас - "далеко не уедешь", так что нам нужно немного развиться, чтобы можно было составить некоторые слова, необходимые для дальнейших действий. Сейчас нам нужно из имеющихся букв получить что-то что даст нам еще больше букв. На данный момент мы можем получить ссылку на один из методов массива. Для этого нам нужно составить имя метода и воспользоваться тем, что JavaScript позволять получать доступ к членам объекта не только с помощью точки и имени, но и с помощью квадратных скобок и строки представляющей имя. То есть вместо [].push мы можем написать []["push"] и это будет иметь тот же эффект. Из имеющихся символов мы можем составить, например, такие слова как fill или filter и этого для данного этапа будет достаточно. Превратив ссылку на функцию в строку уже известным нам способом, мы получим довольно много новых букв. Выражение []+[]["fill"] даст нам следующую строку
"function fill() {
[native code]
}"

Здесь у нас и скобки трех видов и пробел и несколько новых букв, так что добавим эту строку в наш арсенал базовых элементов. И на данный момент наш конструктор класса выглядит вот так
Код javascript Выделить
        constructor()
        {
            this.basics = {
                "true": "([]+!![])",
                "false": "([]+![])",
                "undefined": "([]+[][[]])"
            }
            this.basics["" + [].fill] = `([]+[][${this.encodeText("fill")}])`;
 
        }
И теперь у нас появилась возможность закодировать, например, такое важное слово, как constructor, поскольку все необходимые буквы у нас уже есть. И если сделать вот так "" + [].fill.constructor(), то можно получить следующее
"function anonymous(
) {

}"

Не густо, но там есть пара букв, которые нам тоже пригодятся. Так что дополним код
Код javascript Выделить
    this.basics["" + [].fill.constructor()] = `([]+[][${this.encodeText("fill")}][${this.encodeText("constructor")}]())`;
Также можно добавить еще один словарик, в котором будем хранить слова, исползуемые целиком.
Код javascript Выделить
        this.words = {};
        let constructor = this.words.constructor = this.encodeText("constructor");
        let name = this.words.name = this.encodeText("name");
        let tostring = this.words["toString"] = this.encodeText("to") + `+(([]+[])[${constructor}][${name}])`
Добавили слова constructor, name, toString. Последнее особенно актуально, поскольку это более короткая версия, чем построение по буквам, да и для получения символа S все равно пришлось бы "тянуть" конструктор строки. В базовый словарь слово String тоже можно добавить.
Код javascript Выделить
this.basics["String"] = `+(([]+[])[${constructor}][${name}])`;
На данном этапе, когда у нас появилось слово toString мы получили доступ ко всем буквам латинского алфавита в нижнем регистре. Теперь нам совсем не обязательно "выковыривать" каждую букву изо всех "дыр", в которые они могли затесаться. Для того, чтобы понять, что я имею в виду, надо вспомнить о том, что у типа Number метод toString может принимать аргумент, указывающий на то, в какой системе счисления нужно представить число при переводе его в строку. Поддерживаются системы от 2 до 36. В качестве недостающих цифр в системах с большими основаниями используются буквы латинского алфавита в нижнем регистре. И в системе с основанием 36 используется весь алфавит. Числа мы формировать умеем, toString у нас теперь есть, так что буквы алфавита будут лежать в диапазоне от 10 до 35 и получить их можно вот такими выражениями
Код javascript Выделить
(10)["toString"](36) // a
(11)["toString"](36) // b
....................
(34)["toString"](36) // y
(35)["toString"](36) // z
Соответственно в наш метод encodeText нужно добавить еще одну ветку с условием, и если символа нет в базовом словаре и это не цифра, то будет проверяться не является ли он латинской буквой в нижнем регистре. Таким образом текущая версия этого метода будет следующей.
Код javascript Выделить
        encodeText(text)
        {
            let alphal = "abcdefghijklmnopqrstuvwxyz";
            let digits = "0123456789";
            let allChars = Object.keys(this.basics).join("").split("").filter((v, i, a) => a.indexOf(v) == i);
            let result = [];
            for (let char of text)
            {
                if (allChars.includes(char))
                {
                    for (let w in this.basics)
                    {
                        let index = w.split("").findIndex(c => c == char);
                        if (index > -1)
                        {
                            result.push(`(${"([]+" + this.basics[w] + ")"}[${this.getInteger(index)}])`);
                            break;
                        }
                    }
                }
                else if (digits.indexOf(char) > -1)
                {
                    result.push(this.getDigit(char));
                }
                else if (alphal.indexOf(char) > -1)
                {
                    let n = (alphal.indexOf(char) + 10)
                    result.push(`(+(${this.getInteger(n)}))[${this.words["toString"]}](${this.getInteger(36)})`)
                }
                else
                {
                    return "";
                }
            }
            return result.join("+");
        }
 
    }
Я думаю, вполне понятно, что в последней ветке, где сейчас return "";, должен быть универсальный код, который справится с кодированием любого символа. Есть несколько способов получения символа по его коду, из которых наиболее очевидным является использование метода fromCharCode, но, к сожалению, на данном этапе получить символ C в верхнем регистре - не представляется возможным. Так что пока мы оставим эту затею и перейдем к рассмотрению другого важного вопроса, без которого дальше мы не сможем двигаться.

Работа с функциями



До сего момента мы занимались исключительно созданием словаря для генерации текста и чисел. Но ведь наша задача состоит в том, чтобы исполнить код, да еще при этом и любой код. Понятно, что ключевые слова запрещены, поэтому единственным способом выполнения кода будет отправка текста программы на исполнение, то есть ровно то, что делает функция eval. На самом деле этот способ доступен нам уже сейчас, поскольку мы вполне можем сделать следующее:
Код javascript Выделить
([]["fill"]["constructor"]("alert(1)"))()
Можно выполнить этот код в браузере и он запустит alert(1), а мы передали его в виде текста. Таким образом уже сейчас понятно, что из себя представляет программа на JSFuck, это ровно то, что напсано выше, только вместо алерта перекодированный текст программы и именно поэтому мы столько времени уделили именно генерации текста, поскольку все остальное не представляет никакой сложности.

Рассмотрим, что здесь происходит.
[]["fill"] - возвращает ссылку на метод fill массива. Далее когда мы у него запрашиваем конструктор, мы получаем ссылку на Function, поскольку именна эта функция является в JavaScript конструктором всех функций вообще. Если мы вызываем эту функцию, передав ей в качестве аргумента строку с кодом, то она возвратит нам объект функции ровно с тем кодом, который мы передали. После этого функцию надо всего лишь вызвать.

Но для чего я пишу об этом сейчас, когда еще не получен инструмент, позволяющий генерировать любой текст? Все дело в том, что для получения некоторых символов нам понадобятся некоторые специфические объекты, доступ к которым мы сможем получить только через функции с собственным текстом. То есть функции мы будем использовать еще до того, как запустим код программы. Мало того, мы уже пользовались созданием фукнции, правда без кода, когда использовали конструкцию []+[]["fill"]["constructor"]() для получения слова anonymous.

Итак, наша текущая цель - получение буквы "C" для формирования имени фукнции fromCharCode, которая позволит нам генерировать любой текст. Получить этот символ нам поможет функция escape, как известно эта фукнция кодирует любой текст в URL-формат, то есть, если в текст присутствуют символы, недопустимые для URL она заменяет эти символы на знак процента, за которым следует код символа в шестнадцатиричной системе, то есть помимо цифр используются еще шесть букв латинского алфавита, причем в верхнем регистре и буква C как раз есть среди этих шести. Поэтому перед нами стоит задача - получить ссылку на эту функцию, а также найти какой-нибудь символ, который не разрешен URL-форматом и при этом его код в шестнадцатиричном формате содержит нужную нам букву.

Первая задача решается как раз при помощи создания функции, возвращающей нужную нам фукнцию
Код javascript Выделить
Function("return escape")
В качестве символа, отвечающего описанным выше условиям можно использовать <. А получить мы его можем с помощью одной из фукнций, оборачивающих строку в теги, таких как bold, italics и т. п.
Код javascript Выделить
(Function("return escape")())("".italics())
Данное выражение вернет нам строку "%3Ci%3E%3C/i%3E", в которой присутствует ряд новых символов, включая то что нам нужно. Таким образом мы можем дописать код метода encodeText так, чтобы он справлялся с любым текстом. Добавим в словарик еще пару элементов
Код javascript Выделить
    let fn = `([][${this.encodeText("fill")}][${constructor}])`;
    this.basics["<i></i>"] = `([]+[])[${this.encodeText("italics")}]()`;
    this.basics["%3Ci%3E%3C/i%3E"] = `(${fn}(${this.encodeText("return escape")}))()(${this.basics["<i></i>"]})`;
Последняя ветка encodeText теперь приобретет вид
Код javascript Выделить
    else
    {
        let stringCtor = `(([]+[])[${this.words.constructor}])`;
        let charcode = this.getInteger(char.charCodeAt(0));
        result.push(`${stringCtor}[${this.encodeText("fromCharCode")}](${charcode})`);
    }
Кроме того, теперь нашему классу мы уже можем добавить метод translate, который и будет выполнять первод кода JavaScript в формат JSFuck
Код javascript Выделить
    translate(jscode)
    {
        return `(([][${this.encodeText("fill")}][${this.encodeText("constructor")}])(${this.encodeText(jscode)}))()`;
    }
Теперь создадим страничку, на которой разместим наш транслятор, два текстовых поля, кнопку перевода, и кнопку запуска готового кода. Кроме того будем отображать длину полученного текста программы.
Код phphtml Выделить
<!DOCTYPE html>
 
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <textarea id="js" cols="70" rows="30">alert("hello")</textarea>
    <button onclick="document.getElementById('result').value = jsf.translate(document.getElementById('js').value);
    document.getElementById('charcount').textContent=document.getElementById('result').value.length">
        Encode
    </button>
    <textarea id="result" cols="70" rows="30"></textarea>
    <div id="charcount"></div>
    <button id="run" onclick="eval(document.getElementById('result').value)">Run</button>
    <script>
 
        class JSFuck
        {
            constructor()
            {
                this.basics = {
                    "true": "([]+!![])",
                    "false": "([]+![])",
                    "undefined": "([]+[][[]])"
                }
    
            }
 
            getDigit(d)
            {
                d = +d;
                return "[" + (d == 0 ? "+[]" : Array.apply(null, { length: d }).fill("+!+[]").join("")) + "]";
            }
            getInteger(n)
            {
                return n.toString().split("").map((v) => this.getDigit(+v)).join("+");
            }
 
            encodeText(text)
            {
                let alphal = "abcdefghijklmnopqrstuvwxyz", digits = "0123456789";
                let allChars = Object.keys(this.basics).join("").split("").filter((v, i, a) => a.indexOf(v) == i)
                let result = [];
                for (let char of text)
                {
                    if (allChars.includes(char))
                    {
                        for (let w in this.basics)
                        {
                            let index = w.split("").findIndex(c => c == char);
                            if (index > -1)
                            {
                                result.push(`(${"([]+" + this.basics[w] + ")"}[${this.getInteger(index)}])`);
                                break;
                            }
                        }
                    }
                    else if (digits.indexOf(char) > -1)
                    {
                        result.push(this.getDigit(char));
                    }
                    else if (alphal.indexOf(char) > -1)
                    {
                        let n = (alphal.indexOf(char) + 10)
                        result.push(`(+(${this.getInteger(n)}))[${this.words["toString"]}](${this.getInteger(36)})`)
                    }
                    else
                    {
                        let stringCtor = `(([]+[])[${this.words.constructor}])`;
                        let charcode = this.getInteger(char.charCodeAt(0));
                        result.push(`${stringCtor}[${this.encodeText("fromCharCode")}](${charcode})`);
                    }
                }
                return result.join("+");
            }
            translate(jscode)
            {
                return `(([][${this.encodeText("fill")}][${this.encodeText("constructor")}])(${this.encodeText(jscode)}))()`;
            }
        }
 
        var jsf = new JSFuck();
 
 
    </script>
</body>
</html>
Открыв эту страничку в браузере, можно перекодировать текст JavaScript в формат JSFuck. Таким образом поставленная цель достигнута. Полученный код имеет просто чудовищный размер и размер такой программы в любом случае будет большим, но, в то же время, это не значит, что его вообще нельзя оптимизировать. И хотя оптимизация не входила в задачи этого исследования, тем не менее, думаю, будет небезинтересно рассмотреть некоторые возможности. Кроме того, использовать функцию escape не рекомнедуется, а для получения encodeURI у нас есть не все буквы. Так что далее мы рассмотрим еще несколько второстепенных вопросов.

Использование encodeURI вместо escape



Поскольку функция escape является устаревшей и ее даже обещают в будущем удалить, а она у нас играет достаточно важную роль, неплохо было бы решить проблему ее замены. Конечно можно написать encodeURI вместо escape и вроде бы все готово, но проблема в том, что к тому моменту как мы использовали escape, у нас еще не было букв "URI". Стало быть, для того, чтобы можно было использовать эту функцию нам нужно откуда-то их взять, причем получить их по кодам символов мы не можем, если откажемся от использования escape. Дальше опишу откуда берутся эти буквы.
I можно получить из слова Infifnity
Код javascript Выделить
[]+(+"1e1000")
R можно получить из имени конструктора RegExp
(Function("return/false/"))()["constructor"]["name"]
С U дело немного сложнее, но совсем немного. Мы можем воспользоваться особенностями работы метода toString объекта {}. Если это просто объект, то он выводит его текстовое представление так [object Object] и здесь второе слово - это имя конструктора объекта, причем запсанное с большой буквы, независимо от того, с какой начинается реальное имя конструктора. Как он разрешает ситуации, когда объект не имеет свойств - мне неизвестно, но известно, что если этот метод вызывается для undefined, то результат будет таким [object Undefined], что, в принципе, нашу задачу решает, таким образом букву U мы получим так
Код javascript Выделить
(Function("return{}"))()["toString"]["call"](undefined)
Все это нужно добавить в словарь basics и после этого можно будет исопльзовать encodeURI
Код javascript Выделить
    this.basics["Infinity"] = "([]+(+([+!+[]]+" + this.encodeText("e") + "+[+!+[]]+[+[]]+[+[]]+[+[]]" + ")))";
    let rex = `(${fn})(${this.encodeText("return/a/")})`;
    this.basics["RegExp"] = `${rex}()[${constructor}][${name}]`;
    this.basics["[object Undefined]"] = `${fn}(${this.encodeText("return{}")})()[${tostring}][${this.encodeText("call")}]([][[]])`
    this.basics["%3Ci%3E%3C/i%3E"] = `(${fn}(${this.encodeText("return encodeURI")}))()(${this.basics["<i></i>"]})`;

Несколько мыслей по поводу оптимизации вывода



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

Итак, что мы имеем на данный момент. Откроем нашу страничку и консоль разработчика в браузере. На страничке экземпляр класса JSFuck создан под именем jsf. Попробуем ввести в консоль следующее
Код javascript Выделить
jsf.encodeText("Привет").length
У меня вывело 104661. То есть для того, чтобы закодировать слово "Привет" нужно 104661 символ. По-моему как-то многовато. А если вместо escape использовать encodeURI, то результат будет 157677. Результат увеличился в полтора раза из-за такого мелкого (казалось бы) изменения. Таким образом мы выяснили, что на результат можно существенно повлиять и есть смысл задуматься о том, откуда берутся все эти "тыщи".

Проведем еще эксперимент. При загрузке страницы в первом поле есть код alert("hello"). Если его перекодировать с помощью странички в том виде, в котором она представлена на выше, то получится код из 31843 символов. Теперь добавим в словарик basics всего одну дополнительную запись
Код javascript Выделить
this.basics["".link()] = `([]+[])[${this.encodeText("link")}]()`;
В результате при перекодировании мы получили код из 9303 символов. Нормальная такая экономия.

Причина таких удивительных метаморфоз в том, что весь код на JSFuck - это одно выражение. Здесь нет ни переменных, ни разделителей типа точки с запятой, запятой или новой строки. Таким образом, когда мы фомрировали строительные блоки для получения следующей буквы, в коде транслятора их можно было сохранить в словарик и использовать по мере надобности. Но при генерации кода, всякий раз когда тот или иной элемент понадобится весь его код вставляется в выходной текст полностью. Вспомним, как долго мы шли к тому, чтобы получить латинскую C в верхнем регистре. Все те элементы, которые мы использовали для ее нахождения будут присутствовать когда нам понадобится эта буква. В результате для ее получения нужно 4462 символа при использовании escape. Для того, чтобы получить ее же с помощью encodeURI код будет содержать все те коды, которые мы использовали для получения URI. Там в одном из вариантов у меня получалось больше десяти тысяч символов только для одной буквы. В слове fromCharCode их две, а все слово "стоит" 13010 символов (в упомянутом варианте с encodeURI их было больше 26000). И все эти символы будут вставляться в код всякий раз, когда нам нужно получить символ с помощью fromCharCode. Слово "Привет" написано кирилицей и для каждого символа вызывается fromCharCode. В примере с алертом присутствовали две кавычки. В первом варианте для них вызывался fromCharCode (дважды), а когда в словарь была добавлена запись "".link(), которая содержит кавычки, код получения кавычки стал намного короче, что незамедлительно сказалось на объеме вывода.

Таким образом, по крайней мере одно направление оптимизации мы уже нашли: максимально сократить количество вызовов fromCharCode. Для этого желательно найти максимум возможностей получать символы коротким путем. Здесь нам может помочь тот самый полный листинг....

Правда логику поиска символа в словаре тоже немного придется изменить. Опять-таки маленький пример. Вот код инициализации словарей, который мы используем сейчас.
Код javascript Выделить
    this.basics["" + [].fill] = `([]+[][${this.encodeText("fill")}])`;
    this.basics["" + [].fill.constructor()] = `([]+[][${this.encodeText("fill")}][${this.encodeText("constructor")}]())`;
    this.words = {};
    let constructor = this.words.constructor = this.encodeText("constructor");
    let name = this.words.name = this.encodeText("name");
    let tostring = this.words["toString"] = this.encodeText("to") + `+(([]+[])[${constructor}][${name}])`
    let fn = `([][${this.encodeText("fill")}][${constructor}])`;
    this.basics["<i></i>"] = `([]+[])[${this.encodeText("italics")}]()`;
    this.basics["%3Ci%3E%3C/i%3E"] = `(${fn}(${this.encodeText("return escape")}))()(${this.basics["<i></i>"]})`;
    this.basics["".link()] = `([]+[])[${this.encodeText("link")}]()`;
Для получения латинского символа C требуется 4462 символа. Добавим в словарь записи, которые мы использовали для получения символов URI (именно добавим).
Код javascript Выделить
    this.basics["" + [].fill] = `([]+[][${this.encodeText("fill")}])`;
    this.basics["" + [].fill.constructor()] = `([]+[][${this.encodeText("fill")}][${this.encodeText("constructor")}]())`;
    this.words = {};
    let constructor = this.words.constructor = this.encodeText("constructor");
    let name = this.words.name = this.encodeText("name");
    let tostring = this.words["toString"] = this.encodeText("to") + `+(([]+[])[${constructor}][${name}])`
    let fn = `([][${this.encodeText("fill")}][${constructor}])`;
    this.basics["<i></i>"] = `([]+[])[${this.encodeText("italics")}]()`;
    this.basics["Infinity"] = "([]+(+([+!+[]]+" + this.encodeText("e") + "+[+!+[]]+[+[]]+[+[]]+[+[]]" + ")))";
    let rex = `(${fn})(${this.encodeText("return/a/")})`;
    this.basics["RegExp"] = `${rex}()[${constructor}][${name}]`;
    this.basics["[object Undefined]"] = `${fn}(${this.encodeText("return{}")})()[${tostring}][${this.encodeText("call")}]([][[]])`
    this.basics["%3Ci%3E%3C/i%3E"] = `(${fn}(${this.encodeText("return escape")}))()(${this.basics["<i></i>"]})`;
    this.basics["".link()] = `([]+[])[${this.encodeText("link")}]()`;
В результате тот же символ нам обойдется в 6247 символов выходного потока. Причину причин может быть две:
1. При поиске символва в словаре метод encodeText на длину не обращает внимания. Он находит самый певрый ключ словаря, в котором содержится нужный символ и использует именно этот ключ. Так что нужно либо отсортировать ключи в порядке возрастания длины значения, либо составить новый словарь, в котором каждому символу будет соответствовать наиболее короткий код, из тех, что имеется в словаре. Отсортировать ключи словаря можно, добавив в конце конструктора следующий код
Код javascript Выделить
                let sortedBasics = {},
                    keys = Object.keys(this.basics).sort((k1, k2) => this.basics[k1].length - this.basics[k2].length);
                for (let key of keys)
                {
                    sortedBasics[key] = this.basics[key];
                }
                this.basics = sortedBasics;
2. Среди добавленных в словарь ключей, есть такие, которые содержат буквы, которые лучше получать не из словаря, а с помощью преобразования чисел в 36-ричную систему счисления. Но поскольку сначала мы проверяем наличие символа в словаре, а перевод в 36-СС используем только если символ в словаре не найден, то имеем то, что имеем. Таким образом, наиболее разумным способом будет - найти символ разными способомами и выбрать самый короткий.
Код javascript Выделить
            encodeText(text)
            {
                let alphal = "abcdefghijklmnopqrstuvwxyz", digits = "0123456789";
                let allChars = Object.keys(this.basics).join("").split("").filter((v, i, a) => a.indexOf(v) == i).join("");
                let result = [];
                for (let char of text)
                {
                    if ((alphal + allChars).includes(char))
                    {
 
                        let r1 = "", r2 = "";
                        if (allChars.includes(char))
                        {
                            for (let w in this.basics)
                            {
                                let index = w.indexOf(char);
                                if (index > -1)
                                {
                                    r1 = `(([]+${this.basics[w]})[${this.getInteger(index)}])`;
                                    break;
                                }
                            }
                        }
                        if ("tostring" in this.words && alphal.includes(char))
                        {
                            let n = (alphal.indexOf(char) + 10)
                            r2 = (`(+(${this.getInteger(n)}))[${this.words["tostring"]}](${this.getInteger(36)})`)
                        }
                        result.push(((r1.length < r2.length && r1.length > 0) || r2.length === 0) ? r1 : r2);
                    }
                    else if (digits.indexOf(char) > -1)
                    {
                        result.push(this.getDigit(char));
                    }
                    else
                    {
                        let stringCtor = `(([]+[])[${this.words.constructor}])`;
                        let charcode = this.getInteger(char.charCodeAt(0));
                        result.push(`${stringCtor}[${this.encodeText("fromCharCode")}](${charcode})`);
                    }
                }
                return result.join("+");
            }

Но главная проблема, конечно же, в том, что для перекодирования произвольного текста нам приходится для каждого символа использовать fromCharCode, который нам так дорого достался. И если эта проблема не будет решена, то все описанные выше оптимизации - не более чем "экономия на спичках". Поэтому сосредоточится нужно на решении именно этой проблемы.

Оптимизация работы с fromCharCode



Мы уже видели, что вызов fromCharCode обходится довольно дорого и если перекодировать длинный текст, в котором достаточно много букв будет вызываться этим методом, то объем выходного кода может оказаться в принципе непригодным даже для демонстрации. Поэтому нам нужно подумать, как избежать такого частого вызова этого метода.

Сам метод принимает любое количество аргументов и может за один вызов перекодировать достаточно длинный текст. Казалось бы: вот и решение. Но, не тут-то было. Дело в том, что, имея в распоряжении только шесть символов, мы не так много и можем сделать, по крайней мере, если действовать напрямую. Аргументы фукнции разделяются запятыми, а запятой у нас нет в арсенале. Можно было бы вспомнить такую замечательную штуку, как apply - метод, который принимает аргументы функции, для которой он вызывается, в виде массива. Но проблема в том, что массив аргументов - это второй аргумент apply, а стало быть передать его можно только через запятую. В современных браузерах можно использовать три точки перед массивом, но у нас и точек нет.

Выходом из нашей ситуации может быть создание собственной функции, которая принимала бы массив кодов и возвращала текст, составленный из символов, соответствующих полученным кодам. Правда тут мы сталкиваемся с еще одной той же самой проблемой. Для того, чтобы функция, созданная с помощью Function могла принимать аргументы, конструктор функций сам должен быть вызван с двумя аргументами Function(arglist, code), а если бы у нас была такая возможность, то к этому приему мы вообще не стали бы обращаться.
Тем не менее, возможности языка позволяют нам создать нужную функцию, благодаря коллекции arguments, из которой мы можем получить аргументы функции, даже если она была объявлена без параметров. Таким образом нам нужно создать функцию со следующим кодом.
Код javascript Выделить
return String.fromCharCode.apply(false,arguments[0])
То есть если мы теперь сделаем вот так
Код javascript Выделить
(Function("return String.fromCharCode.apply(false,arguments[0])"))([1055, 1088, 1080, 1074, 1077, 1090])
То такое выражение, возвратит нам слово "Привет", поскольку коды букв именно этого слова были переданы нашей функции. Но тут возникает другой вопрос: а как нам создать массив, ведь там тоже есть запятые и это тоже не запятые в тексте, которые мы вполне можем создать, а именно запятые, как часть кода? Но, на самом деле, эта проблема как раз решается элементарно, а именно - с помощью метода split. Конечно, в результате получится массив строк, но JavaScript легко конвертирует их в числа при передаче функции, требующей числовых аргументов, в этом несложно убедиться, выполнив предыдущий код с маленькими изменениями.
Код javascript Выделить
(Function("return String.fromCharCode.apply(false,arguments[0])"))(["1055", "1088", "1080", "1074", "1077", "1090"])
Таким образом, нам надо выбрать что-то в качестве разделителя и перекодировать текст. Я выбрал в качестве разделителя слово false, поскольку оно может быть закодировано тремя символами и еще потребуется по два плюса, то есть, для кодирования слова привет нам нужно будет сделать следующее
Код javascript Выделить
(Function("return String.fromCharCode.apply(false,arguments[0])"))(("1055"+![]+"1088"+![]+"1080"+![]+"1074"+![]+"1077"+![]+"1090")["split"](![]))
Получив текст таким образом, его надо будет передать на выполнение тем же способом, что и раньше. Добавим в наш класс новый метод translateLarge
На нашу страничку мы добавим еще одну кнопку, для того, чтобы можно было кодировать текст уже с помощью нового метода. И сможем сравнить что получаем в том и другом случае.
Окончательная версия нашей странички для перевода текста имеет следующий вид.
Код phphtml Выделить
<!DOCTYPE html>
 
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <textarea id="js" cols="70" rows="30">alert("hello")</textarea>
    <button onclick="document.getElementById('result').value = jsf.translate(document.getElementById('js').value);
    document.getElementById('charcount').textContent=document.getElementById('result').value.length">
        Encode
    </button>
    <textarea id="result" cols="70" rows="30"></textarea>
    <div id="charcount"></div>
    <button id="runlarge" onclick="eval(document.getElementById('result').value)">Run</button>
    <button onclick="document.getElementById('result').value = jsf.translateLarge(document.getElementById('js').value);
    document.getElementById('charcount').textContent=document.getElementById('result').value.length">
        EncodeLarge
    </button>
    <script>
        class JSFuck
        {
            constructor()
            {
                this.basics = {
                    "true": "([]+!![])",
                    "false": "([]+![])",
                    "undefined": "([]+[][[]])"
                }
                this.words = {};
                this.basics["" + [].fill] = `([]+[][${this.encodeText("fill")}])`;
                let constructor = this.words["constructor"] = this.encodeText("constructor");
                this.basics["" + [].fill.constructor()] = `([]+[][${this.encodeText("fill")}][${constructor}]())`;
                let name = this.encodeText("name");
                this.words.name = name;
                let tostring = this.encodeText("to") + `+(([]+[])[${constructor}][${name}])`
                this.words["tostring"] = tostring;
                let fn = `([][${this.encodeText("fill")}][${constructor}])`;
                this.basics["<i></i>"] = `([]+[])[${this.encodeText("italics")}]()`;
                this.basics["Infinity"] = "([]+(+([+!+[]]+" + this.encodeText("e") + "+[+!+[]]+[+[]]+[+[]]+[+[]]" + ")))";
                let rex = `(${fn})(${this.encodeText("return/a/")})`;
                this.basics["RegExp"] = `${rex}()[${constructor}][${name}]`;
                this.basics["[object Undefined]"] = `${fn}(${this.encodeText("return{}")})()[${tostring}][${this.encodeText("call")}]([][[]])`
                this.basics["%3Ci%3E%3C/i%3E"] = `(${fn}(${this.encodeText("return escape")}))()(${this.basics["<i></i>"]})`;
                this.basics["".link()] = `([]+[])[${this.encodeText("link")}]()`;
                //this.basics[","] = `[[]][${this.encodeText("concat")}]([[]])+[]`;
                let sortedBasics = {},
                    keys = Object.keys(this.basics).sort((k1, k2) => this.basics[k1].length - this.basics[k2].length);
                for (let key of keys)
                {
                    sortedBasics[key] = this.basics[key];
                }
                this.basics = sortedBasics;
            }
 
            getDigit(d)
            {
                d = +d;
                return "[" + (d == 0 ? "+[]" : Array.apply(null, { length: d }).fill("+!+[]").join("")) + "]";
            }
            getInteger(n)
            {
                return n.toString().split("").map((v) => this.getDigit(+v)).join("+");
            }
 
            encodeText(text)
            {
                let alphal = "abcdefghijklmnopqrstuvwxyz", digits = "0123456789";
                let allChars = Object.keys(this.basics).join("").split("").filter((v, i, a) => a.indexOf(v) == i).join("");
                let result = [];
                for (let char of text)
                {
                    if ((alphal + allChars).includes(char))
                    {
 
                        let r1 = "", r2 = "";
                        if (allChars.includes(char))
                        {
                            for (let w in this.basics)
                            {
                                let index = w.indexOf(char);
                                if (index > -1)
                                {
                                    r1 = `(([]+${this.basics[w]})[${this.getInteger(index)}])`;
                                    break;
                                }
                            }
                        }
                        if ("tostring" in this.words && alphal.includes(char))
                        {
                            let n = (alphal.indexOf(char) + 10)
                            r2 = (`(+(${this.getInteger(n)}))[${this.words["tostring"]}](${this.getInteger(36)})`)
                        }
                        result.push(((r1.length < r2.length && r1.length > 0) || r2.length === 0) ? r1 : r2);
                    }
                    else if (digits.indexOf(char) > -1)
                    {
                        result.push(this.getDigit(char));
                    }
                    else
                    {
                        let stringCtor = `(([]+[])[${this.words.constructor}])`;
                        let charcode = this.getInteger(char.charCodeAt(0));
                        result.push(`${stringCtor}[${this.encodeText("fromCharCode")}](${charcode})`);
                    }
                }
                return result.join("+");
            }
            translate(jscode)
            {
                return `(([][${this.encodeText("fill")}][${this.encodeText("constructor")}])(${this.encodeText(jscode)}))()`;
            }
 
            translateLarge(jscode)
            {
                let fncode = this.encodeText("return String.fromCharCode.apply(false,arguments[0])");
                let fn = `([][${this.encodeText("fill")}][${this.encodeText("constructor")}])`;
                let ccfn = `((${fn})(${fncode}))`
                let charCodes = jscode.split("").map(c => this.getInteger(c.charCodeAt(0))).join("+![]+");
                let codeArray = `(${charCodes})[${this.encodeText("split")}](![])`;
                let code = `${ccfn}(${codeArray})`;
                return `(([][${this.encodeText("fill")}][${this.encodeText("constructor")}])(${code}))()`;
            }
        }
 
        var jsf = new JSFuck();
 
    </script>
</body>
</html>

Немного тестов



Теперь, когда код написан, будет совсем не лишним посмотреть объем того, что выдает старый код, новый код(оптимизированный) и код на странице Мартина Клеппе JSFuck - Write any JavaScript with 6 Characters: []()!+
Я буду отображать три числа в следующем порядке:
1. Количество символов, генерируемое вышеуказанной страницей
2. Количество символов, генерируемое первоначальным кодом(кнопка Encode)
3. Количество символов, генерируемое оптимизированным кодом (кнопка EncodeLarge)

Сначала пробуем код alert(1)
1227 1536 85579
Прямо скажем: результат результат не очень, причем неоптимизированный код как-то худо-бедно близок к тому, что выдает гуру JSFuck, а оптимизированный выдает что-то запредельное.
Но не будем останавливаться на этом простом примере и пойдем дальше. Проверим код
Код javascript Выделить
var x="";for(let i=0;i<10;i++)x+=i*i;alert(x)
Результат:
53113 133965 87381
Здесь код Мартина Клеппе опять выдал наиболее короткий вариант, а вот в моих кодах уже есть некоторые изменения. А именно: оптимизированный вариант уже начал оправдывать то, что я называю его оптимизированным. При этом следует заметить, что по сравнения с предыдущим кодом, размер выходного текста изменился но не сильно( это важно).

Теперь попробуем закинуть туда что-нибудь подлиннее. Например код нашего класса JSFuck целиком. И вот какие результаты мы получим

3673690 6791169 310945

И оказалось, что оптимизированный транслятор выдал результат более чем в десять раз короткий по сравнению с транслятором Мартина Клеппе, а сравнение с моим неоптимизированным кодом еще более внушительное.

Объяснение всему этому достаточно простое. Мы достаточно много символов потратили на то, чтобы создать удобную функцию, позволяющую транслировать коды символов. Весь этот инструментарий генерирует порядка 85 тыс. символов. Поэтому, каким бы коротким ни был код, результат при использовании этого метода не может быть меньше примерно этого числа. Зато все остальные символы, независимо от того, что это за символы, будут "весить" относительно немного.

И снова музыка VK

  1. Предварительные замечания
  2. Получение данных о треке
  3. Получение прямой ссылки
  4. Скрипт для Tampermonkey

Предварительные замечания



На тему скачивания музыки из социальной сети "В контакте" я уже писал ранее, но с тех пор кое-что изменилось, так что скрипты, актуальные для того времени - больше не работают. Мало того, "вконтактик" стал "шифроваться": если раньше страница содержала прямые ссылки на аудиофайлы, то теперь их там нет и процесс существенно усложнился.

Далее следует упомянуть, что для скачивания музыки из "вконтактика" есть расширения для браузеров, так что, если нужно просто что-то скачать, то лучше использовать именно их, а то, что я опишу здесь, будет интересно скорее, если надо сделать что-то нестандартное. Например, ранее я приводил скрипт для скачивания видео оттуда же, так там в браузере просто формировался bash-скрипт для скачивания, после чего его можно разместить на какой-то другой машине, которая будет все это дело качать. Для музыки это, возможно, и не так актуально, поскольку музыкальные файлы меньше по размеру и качаются быстро, но, тем не менее сценарии, в которых может потребоваться получение списка ссылок на аудиофайлы - вполне могут быть востребованными. Хотя, лично для меня весь этот процесс представляет сугубо "научный" интерес.


Получение данных о треке



Как уже было сказано выше, страница с музыкой "вконтактика" не содержит прямых ссылок на аудиофайлы ни в каком виде. Мало того, там и другая информация о треках появляется только по мере прокрутки страницы и появления их описания в поле видимости. В то же время браузеру, для того, чтобы что-то воспроизвести, надо знать откуда это "что-то" надо качать. Это означает, что в какой-то момент нужная ссылка все-таки появляется и нам надо понять когда и как это происходит. Наиболее логичным будет предположение, что информация о файле запрашивается с сервера в тот самый момент, когда пользователь нажимает на "кнопку" воспроизведения. Исходя из этого предположения, нам надо открыть страницу с музыкой и инструменты разработчика браузера(вместо инструментов это может быть и внешняя программа, позволяющая мониторить траффик), запустить какой-нибудь трек и посмотреть, что после этого подгрузилось. Возможно нам повезет и мы найдем что-то похожее на ссылку на аудиофайл.

Вот я взял для примера один трек, запустил его и среди прочих ответов нашел один следующего содержания
Код:
3555327223824<!><!>0<!>6864<!>0<!><!json>[[456239027,236521131,"https:\/\/vk.com\/mp3\/audio_api_unavailable.mp3?extra=yvHQuL0Tn1DYsZiUu3boDgvJsuLLCgzHq2vUnMXkCvv1D105uOzyuOu5otvSvxjuvO9tBxqWmhDYqKq5AZDRlMm5DeTSChbnqs8WDujVCMvtrw12CdDwELzIncOXwu1zyMvZq3GWAwjgnODZmez6qZfdsermoc9sse8ZnfbFBvHAlJjdCdboogK5AsO9AdHqCMvsnwrZm24VC3q5zZHJBNy4AuHXEhaVsxq5zun3nJLSyZm4p286Eerpsu9TmfvdA1DrmLvOCW#CWS2mde","Toxicity","SOAD",221,0,0,"",0,66,"","[]","87ef4ee798155bb9c7\/5af6233c4ebfce2bc7\/dbcca94dfef4740bcb\/1b09969c356c6cd7b7\/","https:\/\/pp.userapi.com\/c637525\/v637525450\/43363\/fq34v93oQ-E.jpg,https:\/\/pp.userapi.com\/c637525\/v637525450\/43362\/Empa6PNfdd4.jpg",13],[456239026,236521131,"https:\/\/vk.com\/mp3\/audio_api_unavailable.mp3?extra=Ady1DZHZyxa3mOTcAY5tlKTem3iYrtqZEdi3CtDLvvvFyNfHvLj2BtfSmgKWqNnWzxmOB1DHBI5KodmZELKWuxPdr2mVytrdqN0Wus9bBMH1ANfrs2DRohnZAZuWrxbHyvjwB3POwxu1AMTWCMLPxOPiy2e2uhHTnJiXqtPzBdqVuxbZtKTrmN09qwTOsJKWrK9kttHzDOjVBM1yDxvNvMOYA20YpZHYufu3A3GTmg9Vy3qTC3LcBwvJss9tDLzJvMmVBZu1x3rertnMsfH4vtzsls9zuffY#CWS5nJG","Bohemian Raphsody","Queen",367,0,0,"",0,2,"","[]","de85fcf68b66fdfc72\/20835a3ec770f404a1\/143a1ba9f6cf77e927\/98f642833f224eebc4\/","",[]],[456239021,236521131,"https:\/\/vk.com\/mp3\/audio_api_unavailable.mp3?extra=qL05v1btyxb4lZzWAwnMzfi5CvrLutrrmhrpz3qXnhjLt3zkrM54tNmOwMvslNPMufy9B1D4EwnoCOH1BxfiEdbixZi2CNrOs1bwAfLNqxr4BOjkzK84CvvHmdD1ttq2Cxe\/Bhe3Cfn0CY4TlZn2Es8WAtzJEwe6mfbqzeTjoejZuKrdovvcmLu5DdfkvLPyoc53m2HIlwXjCePZuuPTAM44z3bUAxHTyvDLmfrinKrVCMnjvxLSwezJrM5Yx2vXrOy1lZu1vq#CWS4mJC","Прелюдия op.23 N5 g-moll (C. Рихтер)","C. Рахманинов",224,0,0,"",0,66,"","[]","72dd9492b75b10c6d2\/cb7082fdf361019a23\/aedd4d728dcd893ab6\/ccb81055a794889803\/","https:\/\/pp.userapi.com\/c604720\/v604720447\/4d59e\/3FEenUEGjcc.jpg,https:\/\/pp.userapi.com\/c604720\/v604720447\/4d59d\/mK7PAW1l6_8.jpg",32]]<!><!json>{"236521131":"1"}<!>970abaeddddd19fa50
Здесь мы видим JSON-код, обрамленный некими дополнительными маркерами, представляющий массив массивов, где каждый внутренний массив содержит информацию об отдельном треке, в том числе адрес некоего файла с расширением mp3, хоть и с очень подозрительным именем файла audio_api_unavailable, напоминающем нам о недоступности аудио-апи. Но поскольку у адреса есть еще параметр, то остается надежда, что это то, что нам нужно. Но, к сожалению, эта ссылка ведет к файлу, который теперь уже голосом рассказывает нам о том, что мы пытаемся прослушать файл не тем приложением или что-то в этом роде.

Несмотря на то, что везде в этих примерах мы получаем один и тот же файл, все-таки параметр extra у каждого адреса свой, поэтому, как показали дальнейшие исследования, вожделенную прямую ссылку мы, в конце концов, будем получать именно из него. Стало быть для начала было бы совсем не лишним разобраться как получить от сервера такой же ответ.

Для получения такого ответа был отправлен POST-запрос на адрес https://vk.com/al_audio.php, со следующими параметрами
act: reload_audio
al: 1
ids: 236521131_456239027,236521131_456239026,236521131_456239025,236521131_456239024,236521131_456239021,236521131_456239027

Первые два параметра всегда одинаковые, что касается третьего, то нетрудно догадаться, что здесь записаны через запятую уникальные идентификаторы треков. Здесь указано пять идентификаторов, хотя мы получили информацию только о трех треках, но на странице, с которой я это взял в списке среди первых пяти треков третий и четвертый стали доступны только по подписке, поэтому информация о них и не пришла.

Кроме изложенного следует упомянуть о cookies. Если запрос отправляется из браузера с учетом контекста страницы, как это происходит, если выполнять все в скрипте GreaseMonkey или ему подобных, то можно об этом не думать. В то же время, если надо отправить запрос из стороннего приложения, или даже из браузера, но когда не пройдена авторизация в vk, то вразумительного ответа от сервера не будет. Пока мы на этом сосредотачиваться не будем, а займемся идентификаторами.

Открываем страничку с аудиозаписями берем любой трек и просматриваем блок с записью с помощью инструментов разработчика. Я смотрю в файрфоксе, там в контекстном меню есть пункт "Исследовать элемент". В одном из контейнеров, открытого таким образом элемента можно обнаружить атрибут data-full-id в котором как раз и содержится нужный нам id. Этой информации вполне достаточно для того, чтобы собрать ids со страницы.
Код javascript Выделить
function GetVisibleIds()
{
  var audiorows = document.querySelectorAll("div[data-full-id]");
  return Array.prototype.map.call(audiorows, function(e){return e.getAttribute("data-full-id")});
}
Для получения данных с сервера по идентификаторам треков напишем пару фунций: синхронную и асинхронную. На синхронные браузер не очень хорошо реагирует и пишет в консоли, что не надо так делать, хотя и выполняет все как надо. А если выполнять параллельно несколько таких запросов, то сервер может плохо отреагировать, хотя это только предположение. Поэтому пишу в двух вариантах, хотя делают эти функции одну и ту же работу, а что использовать - каждый решит сам.
Кликните здесь для просмотра всего текста
Код javascript Выделить
function GetTrackDataAsync(ids, callback)
{
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "https://vk.com/al_audio.php?act=reload_audio&al=1&ids=" + ids, true);
  xhr.onreadystatechange = function() 
  {
      if (xhr.readyState != 4) return;
 
      if (xhr.status != 200) 
      {
        console.log(xhr.status + ': ' + xhr.statusText);
      } 
      else 
      {
        //console.log("response");
        var re = /\<\!json\>([^<]+)\<!>/;
        var resp = re.exec( xhr.responseText)[1];
        callback(JSON.parse(resp));
      }
 
  }
  xhr.send();
  
}
 
function GetTrackData(ids)
{
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "https://vk.com/al_audio.php?act=reload_audio&al=1&ids=" + ids, false);
  xhr.send();
  var re = /\<\!json\>([^<]+)\<!>/;
  var resp = re.exec( xhr.responseText)[1];
  return JSON.parse(resp);
}
Далее следует сказать, что при отправке более пяти идентификаторов одновременно адекватного ответа ждать не стоит. Кроме того надо помнить, что некоторые треки доступны только по подписке или недоступны по другим причинам и информация о них не приходит. Так что надо доступные id разделить на группы по 5 элементов и смотреть, что возвратил сервер, благо ответ содержит информацию о треке, которая не исчерпывается ссылкой на файл. Идентификатор полученного трека можно получить объединив первые два элемента массива знаком подчеркивания, третий элемент - url файла с которым будем дальше работать для получения прямой ссылки, далее идут: название композиции, автор, продолжительность в секундах. В остальном не разбирался, поскольку решил, что это вряд ли понадобится.

Получение прямой ссылки



Теперь рассмотрим как из полученного негодного адреса файла получить ссылку на файл, который мы ищем. Получить информацию о том, как на странице обрабатываются данные - задача непростая, поскольку придется перелопатить очень много всего. Я пошел по более простому пути и воспользовался результатом работы проделанной другими людьми. А именно: установил расширение для браузера Firefox, предназначенное скачивания аудиофайлов оттуда же, откуда качаем и мы и поискал решение там. Ссылка на это расширение здесь.VK Universal Downloader.

Исследование кода этого расширения привело к тому, что нам понадобится следующий фрагмент.
Кликните здесь для просмотра всего текста
Код javascript Выделить
function fix_direct_url(t) {
    if (~t.indexOf("audio_api_unavailable")) {
        var e = t.split("?extra=")[1].split("#"), o = "" === e[1] ? "" : a(e[1]);
        if (e = a(e[0]), "string" != typeof o || !e)return t;
        o = o ? o.split(String.fromCharCode(9)) : [];
        for (var s, r, n = o.length; n--;) {
            if (r = o[n].split(String.fromCharCode(11)), s = r.splice(0, 1, e)[0], !l[s])return t;
            e = l[s].apply(null, r)
        }
        if (e && "http" === e.substr(0, 4))return e
    }
    return t
}
 
function s(t, e) {
    var i = t.length, o = [];
    if (i) {
        var a = i;
        for (e = Math.abs(e); a--;)o[a] = (e += e * (a + i) / e) % i | 0
    }
    return o
}
 
var r = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/=", l = {
    v: function (t) {
        return t.split("").reverse().join("")
    }, r: function (t, e) {
        t = t.split("");
        for (var i, o = r + r, a = t.length; a--;)i = o.indexOf(t[a]), ~i && (t[a] = o.substr(i - e, 1));
        return t.join("")
    }, s: function (t, e) {
        var i = t.length;
        if (i) {
            var o = s(t, e), a = 0;
            for (t = t.split(""); ++a < i;)t[a] = t.splice(o[i - 1 - a], 1, t[a])[0];
            t = t.join("")
        }
        return t
    }, x: function (t, e) {
        var i = [];
        return e = e.charCodeAt(0), each(t.split(""), function (t, o) {
            i.push(String.fromCharCode(o.charCodeAt(0) ^ e))
        }), i.join("")
    }
}
 
function a(t) {
    if (!t || t.length % 4 == 1)return !1;
    for (var e, i, o = 0, a = 0, s = ""; i = t.charAt(a++);)i = r.indexOf(i), ~i && (e = o % 4 ? 64 * e + i : i, o++ % 4) && (s += String.fromCharCode(255 & e >> (-2 * o & 6)));
    return s
}
Единственная неприятность, что там используется некая функция each, которая нигде не определена, и по всей видимости импортируется в этот код, но откуда - я не разбирался. В то же время, несложно догадаться, что именно делает эта функция, поэтому я ее добавил и реализовал следующим образом.
Код javascript Выделить
        function each(arr, f)
        {
            for (var i = 0; i < arr.length; i++)
            {
                f(arr, arr[i]);
            }
        }


Скрипт для Tampermonkey


Теперь можно все вышесказанное объединить во что-то более-менее работающее и способное проиллюстрировать все вышесказанное. Скрипт запускался в браузере Google Chrome с расширением Tampermonkey. В Firefox, к сожалению на vk.com это расширение по непонятной причине не хочет реагировать на GM_registerMenuCommand и не создает пункт меню. Я написал об этом в комментариях на странице расширения, разработчик в ответ предложил скачать бета-версию "from here", но где это "here" указать, видимо, забыл. Кроме того попросил, напсать работет или нет, но куда писать - тоже неизвестно. )) Поэтому пока тестировалось только на хроме.
Кликните здесь для просмотра всего текста
Код javascript Выделить
// ==UserScript==
// @name        DownloadVkMusic
// @namespace   https://vk.com/
// @match       https://vk.com/*
// @version     1
// @description  Получает данные о музыкальных треках на vk.com
// @author       diadiavova
// @grant       GM_registerMenuCommand
// ==/UserScript==
 
(function(){
    GM_registerMenuCommand("Показать данные о треках", function(){
        TextInNewPage(JSON.stringify(GetDataFromPage().map(GetObjFromArray)));
    });
    function GetVisibleIds()
    {
        var audiorows = document.querySelectorAll("div[data-full-id]");
        return Array.prototype.map.call(audiorows, function(e){return e.getAttribute("data-full-id");});
    }
 
    function GetTrackDataAsync(ids, callback)
    {
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "https://vk.com/al_audio.php?act=reload_audio&al=1&ids=" + ids, true);
        xhr.onreadystatechange = function()
        {
            if (xhr.readyState != 4) return;
 
            if (xhr.status != 200)
            {
                console.log(xhr.status + ': ' + xhr.statusText);
            }
            else
            {
                var re = /<\!json\>([^<]+)<!>/;
                var resp = re.exec( xhr.responseText)[1];
                callback(JSON.parse(resp));
            }
        };
        xhr.send();
 
    }
 
    function GetTrackData(ids)
    {
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "https://vk.com/al_audio.php?act=reload_audio&al=1&ids=" + ids, false);
        xhr.send();
        var re = /<\!json\>([^<]+)<!>/;
        var resp = re.exec( xhr.responseText)[1];
        return JSON.parse(resp);
    }
 
 
    function GetIdRanges()
    {
        var idarray = GetVisibleIds();
        var result = [];
        for(var i = 0; i < idarray.length; i += 5)
        {
            result.push(idarray.slice(i, i + 5).join(","));
        }
        return result;
    }
 
    function GetDataFromPage()
    {
        var result = [];
        for(var ids of GetIdRanges())
        {
            result = result.concat(GetTrackData(ids));
        }
        return result;
    }
 
    function GetObjFromArray(a)
    {
        return {author: a[4], track: a[3], url: fix_direct_url(a[2]), duration: a[5]};
    }
 
    function TextInNewPage(txt)
    {
 
        var newwin = unsafeWindow.open("about:blank");
        setTimeout(function(){newwin.document.body.appendChild(document.createTextNode(txt));}, 1000);
 
    }
 
 
 
 
    // Честно стыренный код расшифровки имен файлов
 
    function fix_direct_url(t) {
        if (~t.indexOf("audio_api_unavailable")) {
            var e = t.split("?extra=")[1].split("#"), o = "" === e[1] ? "" : a(e[1]);
            if (e = a(e[0]), "string" != typeof o || !e)return t;
            o = o ? o.split(String.fromCharCode(9)) : [];
            for (var s, r, n = o.length; n--;) {
                if (r = o[n].split(String.fromCharCode(11)), s = r.splice(0, 1, e)[0], !l[s])return t;
                e = l[s].apply(null, r);
            }
            if (e && "http" === e.substr(0, 4))return e;
        }
        return t;
    }
 
    function s(t, e) {
        var i = t.length, o = [];
        if (i) {
            var a = i;
            for (e = Math.abs(e); a--;)o[a] = (e += e * (a + i) / e) % i | 0;
        }
        return o;
    }
 
    var r = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/=", l = {
        v: function (t) {
            return t.split("").reverse().join("");
        }, r: function (t, e) {
            t = t.split("");
            for (var i, o = r + r, a = t.length; a--;)i = o.indexOf(t[a]), ~i && (t[a] = o.substr(i - e, 1));
            return t.join("");
        }, s: function (t, e) {
            var i = t.length;
            if (i) {
                var o = s(t, e), a = 0;
                for (t = t.split(""); ++a < i;)t[a] = t.splice(o[i - 1 - a], 1, t[a])[0];
                t = t.join("");
            }
            return t;
        }, x: function (t, e) {
            var i = [];
            return e = e.charCodeAt(0), each(t.split(""), function (t, o) {
                i.push(String.fromCharCode(o.charCodeAt(0) ^ e));
            }), i.join("");
        }
    };
 
    function a(t) {
        if (!t || t.length % 4 == 1)return !1;
        for (var e, i, o = 0, a = 0, s = ""; i = t.charAt(a++);)i = r.indexOf(i), ~i && (e = o % 4 ? 64 * e + i : i, o++ % 4) && (s += String.fromCharCode(255 & e >> (-2 * o & 6)));
        return s;
    }
})();


Данный скрипт создает пункт меню "Показать данные о треках" в меню расширения на странице вконтктика. Если на странице отображены треки, то в результате работы скрипта откроется новая вкладка и через секунду в ней отобразится JSON с данными о треках, включая прямые ссылки. Для отображения данных пришлось использовать setTimeout при открытии нового окна, поскольку обработка DOMContentLoaded в хроме почему-то не работает.

Полученный документ надо использовать сразу, поскольку прямые ссылки на файлы со временем устаревают.

Разбиение коллекции на диапазоны средствами LINQ.

  1. Задача с форума
  2. Попробуем обобщить

Задача с форума


Недавно на форуме попался достаточно простой вопрос - как получить длину самой длинной последовательности чисел, идущих подряд (то есть где каждое последующее число диапазона больше предыдущего на единицу). Решение задачи достаточно простое и обсуждать тут особенно нечего, но я задался вопросом, как это сделать средствами LINQ, причем без использования свертки, иначе там логика была примерно такой же, что и при решении циклами. Там я выложил вот такое решение этой задачи
Код csharp Выделить
            int[] ia = { 1, 0, -1, 2, 3, -2, 4, 5, 6, -3, -4, 7, 8, 9, 10, 11};
            var qqq = ia.Zip(ia.Skip(1), (i1, i2) => i2 - i1 == 1).Concat(new bool[] { false}).Select((b, i) => b ? 0 : i).Where(x => x > 0);
            var maxCount = qqq.Zip(qqq.Skip(1), (x, y) => y - x).Max();
            Console.WriteLine(maxCount);
Несмотря на то, что, как потом выяснилось, это решение в некоторых случаях не работает, тем не менее в конечном варианте логика останется такой же, а модификация кода будет незначительной. Учитывая, что данный код выглядит немного непонятно, думаю, дать пояснения по поводу того, что здесь происходит - будет совсем не лишним.
Первоначальный вариант выглядел так
Код csharp Выделить
            int[] ia = { 1, 0, -1, 2, 3, -2, 4, 5, 6, -3, -4, 7, 8, 9, 10, 11, -4, 12, 13};
            var qqq = ia.Zip(ia.Skip(1), (i1, i2) => i2 - i1 == 1).Select((b, i) => b ? 0 : i).Where(x => x > 0);
            var maxCount = qqq.Zip(qqq.Skip(1), (x, y) => y - x).Max();
            Console.WriteLine(maxCount);
Это сработало правильно, одну проблему я заметил и исправил сразу, но сначала объясню на этом - немного более простом - примере, поскольку в окончательном варианте логика будет той же.

Для выявления диапазонов, в которых должны сравниваться соседние элементы здесь используется сочетание функций Zip и Skip. Первая объединяет две коллекции попрано, а вторая(переданная с аргументом 1) сдвигает элементы коллекции на одну позицию. Таким образом (поскольку источником обеих функций является одна и та же коллекция) мы объединяем первый элемент со вторым, второй с третьим и т. д. Второй аргумент функции Zip - проверяет, действительно ли элемент i2 больше элемента i1 на единицу. В результате такой операции мы получим булеву коллекцию, в которой каждый элемент будет указывать, относятся ли соответствующие ему соседние элементы исходной коллекции к одному диапазону (отличаются ли на единицу). Таким образом, если в каком-то месте исходной коллекции был диапазон из трех последовательных чисел, то в полученной нами коллекции на его месте будет диапазон из двух элементов true. Например диапазон 7 8 9 даст true для разницы между 7 и 8, и еще true для разницы 8 и 9.
Далее с помощью функции Select мы заменяем элементы true нулями, а false - их индексами в нашей булевой коллекции. После этого с помощью функции Where удаляем нули.
Теперь нам надо разобраться, что же мы имеем на данном этапе и что нам это дает. Представим себе, что в исходной булевой коллекции у нас есть следующая последовательность где-то в середине этой коллекции.
Код:
false true true true false
Наличие трех true подряд, как мы уже выяснили выше, означает, что в соответствующем фрагменте исходной коллекции мы имели четыре подряд идущих числа. Допуситм, что первый false имел индекс 6, тогда соответствующие индексы для этого фрагмента распределятся следующим образом
Код:
false 6
true 7
true 8
true 9
true 10
Теперь, если мы заменим true - нулями, а false - их индексами, мы получим последовательность
Код:
6 0 0 0 10
И после удаления нулей останется только
Код:
6 10
Теперь, если мы найдем разность между этими соседними элементами (10 - 6 = 4), то в результате у нас получится число, на единицу большее, чем количество удаленных true из этого места, что в точности соответствует количеству элементов последовательности первоначальной коллекции. Таким образом, мы получили коллекцию таких чисел, что найдя разность между соседними элементами этой коллекции мы будем получать длину одного из диапазонов исходной коллекции. Соответсвнно, для получения списка длин, нам снова надо объединить попарно соседние элементы этой коллекции уже знакомым нам способом (Zip + Skip), вычисляя разности в результирующей функции, и найти максимум полученной коллекции разностей.

В приведенном выше коде есть пара нюансов, способных в некоторых случаях привести к ошибке.
  1. Искомая последовательность может оказаться вначале или в конце исходной коллекции. Таким образом булева коллекция будет начинаться (или заканчиваться) с последовательности true, которые будут впоследствии удалены бесследно. Мало того, коллекции может полностью состоять из одного непрерывного диапазона, который даст коллекцию, состоящую полностью из true и она в следующем шаге будет полностью отфильтрована. Для того, чтобы избежать, после получения булевой коллекции ее надо с обеих сторон обрамить элементами false, поскольку именно они после фильтрации будут служить опорными точками. Кроме того, следует учесть, что первый false будет иметь индекс 0, так что при удалении нулей мы его потеряем вместе со всеми true. Чтобы этого избежать, нам надо либо вначале добавить два false, либо вместо нуля для замены использовать отрицательное число и удалять тоже именно его (в итоге я использовал -1).
  2. Если этот код оформить в виде отдельного метода, то будет совсем не лишним предусмотреть вариант, когда методу передается пустая коллекция или коллекция, состоящая из одного элемента.

То есть в итоге мы получим следующее:
Код csharp Выделить
        static int MaxRangeLength(IEnumerable<int> ia)
        {
            if (ia.Count() < 2) return ia.Count();
            var qqq = new bool[] { false }
                .Concat(ia.Zip(ia.Skip(1), (i1, i2) => i2 - i1 == 1))
                .Concat(new bool[] { false })
                .Select((b, i) => b ? -1 : i)
                .Where(x => x > -1);
            return qqq.Zip(qqq.Skip(1), (x, y) => y - x).Max();
        }
Не могу сказать, что провел полноценное тестирование, но минимальные тесты метод прошел и дал хорошие результаты.

Попробуем обобщить



Описанная выше задача имела достаточно частный характер и, по всей видимости, нужна была только конкретному студенту. Однако же задача деления коллекции на диапазоны может иметь и прикладной характер, поэтому попробуем создать метод расширения для IEnumerable<T>, который будет выполнять эту задачу. Единственное, с чем нам нужно определиться, это с критерием, по которому мы будем определять, где заканчивается один диапазон и начинается другой. Обычно в диапазоне соседние элементы как-то связаны, например, в задаче выше, для того, чтобы элемент входил в тот же диапазон, что и предыдущий элемент, он должен быть на единицу больше его. То есть в качестве аргумента нашего метода мы можем использовать делегат, который будет принимать два соседних элемента коллекции и возвращать булево значение, которое будет показывать, входят эти элементы в один диапазон или в разные. То есть нам нужно реализовать метод со следующей сигнатурой.
Код csharp Выделить
        public static IEnumerable<IEnumerable<T>> SplitByRanges<T>(this IEnumerable<T> ie, Func<T, T, bool> condition)
        {
 
        }
Теперь о реализации. Первая часть этого метода очень похожа на то, что мы делали выше. Мы получаем индексы границ диапазонов, причем нам достаточно получить верхние границы. Фактически мы будем получать список чисел, каждое из которых на единицу превышает индекс последнего элемента одного из диапазонов в исходной коллекции.
Код csharp Выделить
            var indecies = ie
                .Zip(ie.Skip(1), condition)
                .Concat(new bool[] { false })
                .Select((item, index) => item ? -1 : index + 1)
                .Where(i => i > -1);
Здесь мы добавляем false только в конец булевой коллекции, поскольку нам надо вычислить только верхние границы диапазонов. Остальное более-менее понятно всем кто прочитал предыдущий пример.

Теперь нам надо определиться с тем, как мы будем делить одну коллекцию на коллекцию коллекций. Как уже ранее говорилось, решить задачу надо без использования циклов, свертки и рекурсии, поэтому надо решение свести к использованию функций, которые могут дать в результате объект такого типа. Я решил использовать для этих целей GroupBy. В результате получился следующий код.
Код csharp Выделить
        public static IEnumerable<IEnumerable<T>> SplitByRanges<T>(this IEnumerable<T> ie, Func<T, T, bool> condition)
        {
 
            var indecies = ie
                .Zip(ie.Skip(1), condition)
                .Concat(new bool[] { false })
                .Select((item, index) => item ? -1 : index + 1)
                .Where(i => i > -1);
 
            return ie
                   .Select((item, index) => new { item, key = indecies.First(x => index < x) })
                   .GroupBy(x => x.key, (k, e) => e.Select(ee => ee.item));
        }
Здесь каждому элементу исходной коллекции сопоставляется ключ, получаемый следующим образом: в коллекции индексов находится самый первый индекс, превышающий индекс текущего элемента исходной коллекции и именно он и будет использоваться как ключ. Таким образом, если в коллекции индексов у нас есть числа 3 и 8, то элементы с индексами 0 1 2, получат ключ 3, а элементы с индексами 3 4 5 6 7, получат ключ 8 и т. д. Далее осуществляется группировка по этому ключу, а поскольку группируются объекты, содержащие элемент и ключ, то полученные группы надо преобразовать в коллекции элементов, для этого используется перегрузка GroupBy, включающая функцию-селектор.

Тестируем метод следующим образом.
Код csharp Выделить
            int[] aaa = { 1, 2, 3, 5, 6, 7, 8, 3, 3, 4, 5, 6, 7, 8, 9, 1, 1, 1 };
            foreach (IEnumerable<int> aa in aaa.SplitByRanges((x, y) => y - x == 1))
            {
                foreach(int a in aa)
                {
                    Console.Write(a + " ");
                }
                Console.WriteLine();
            }
Здесь мы передали нашему методу функцию, которая отбирает в группы подряд идущие элементы(следующий на единицу больше предыдущего). На выходе получаем
Код:
1 2 3
5 6 7 8
3
3 4 5 6 7 8 9
1
1
1
Ну и теперь можно гораздо проще решить "задачу с форума"
Код csharp Выделить
Console.WriteLine(aaa.SplitByRanges((x, y) => y - x == 1).Select(i => i.Count()).Max());