Статьи

Сканирование с помощью Node, PhantomJS и Horseman

Эта статья была рецензирована Лукасом Уайтом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

В ходе проекта довольно часто возникает необходимость написания пользовательских сценариев для выполнения различных действий. Такие одноразовые сценарии, которые обычно выполняются через командную строку ( CLI ), могут использоваться практически для любого типа задач. За многие годы написав много таких сценариев, я осознал, что стоит потратить небольшое количество времени на предварительную разработку собственной микросхемы CLI для облегчения этого процесса. К счастью, Node.js и его обширная экосистема пакетов, npm , позволяют легко сделать это. Разбираете ли вы текстовый файл или запускаете ETL , наличие соглашения позволяет легко добавлять новые функции эффективным и структурированным способом.

Хотя это не обязательно связано с командной строкой, сканирование в Интернете часто используется в определенных проблемных областях, таких как автоматическое функциональное тестирование и обнаружение порчи. Этот учебник демонстрирует, как реализовать облегченную среду CLI, поддерживаемые действия которой вращаются вокруг веб-сканирования. Надеюсь, это поможет вам создать креативные соки, будь то ваш интерес к ползанию или к командной строке. Рассматриваемые технологии включают Node.js, PhantomJS и ассортимент пакетов npm, относящихся как к сканированию, так и к CLI.

Исходный код этого руководства можно найти на GitHub . Для запуска примеров вам понадобится установить Node.js и PhantomJS. Инструкции по их загрузке и установке можно найти здесь: Node.js , PhantomJS .

Настройка базовой инфраструктуры командной строки

В основе любой инфраструктуры CLI лежит концепция преобразования команды, которая обычно включает в себя один или несколько необязательных или обязательных аргументов, в конкретное действие. Два пакета npm, которые весьма полезны в этом отношении, являются commander и prompt .

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

Скажем, например, мы хотим, чтобы наша команда выглядела так:

$ node run.js -x hello_world 

Наша точка входа ( run.js ) определяет возможные аргументы, например:

 program .version('1.0.0') .option('-x --action-to-perform [string]', 'The type of action to perform.') .option('-u --url [string]', 'Optional URL used by certain actions') .parse(process.argv); 

и определяет различные случаи пользовательского ввода, как это:

 var performAction = require('./actions/' + program.actionToPerform) switch (program.actionToPerform) { case 'hello_world': prompt.get([{ // What the property name should be in the result object name: 'url', // The prompt message shown to the user description: 'Enter a URL', // Whether or not the user is required to enter a value required: true, // Validates the user input conform: function (value) { // In this case, the user must enter a valid URL return validUrl.isWebUri(value); } }], function (err, result) { // Perform some action following successful input performAction(phantomInstance, result.url); }); break; } 

На данный момент мы определили базовый путь, по которому мы можем указать действие, которое нужно выполнить, и добавили приглашение принять URL. Нам просто нужно добавить модуль для обработки логики, специфичной для этого действия. Мы можем сделать это, добавив файл с именем hello_world.js в каталог действий :

 'use strict'; /** * @param Horseman phantomInstance * @param string url */ module.exports = function (phantomInstance, url) { if (!url || typeof url !== 'string') { throw 'You must specify a url to ping'; } else { console.log('Pinging url: ', url); } phantomInstance .open(url) .status() .then(function (statusCode) { if (Number(statusCode) >= 400) { throw 'Page failed with status: ' + statusCode; } else { console.log('Hello world. Status code returned: ', statusCode); } }) .catch(function (err) { console.log('Error: ', err); }) // Always close the Horseman instance // Otherwise you might end up with orphaned phantom processes .close(); }; 

Как вы можете видеть, модуль ожидает поставки экземпляра объекта phantomInstance ( phantomInstance ) и URL ( url ). Мы кратко рассмотрим особенности определения экземпляра PhantomJS, но на данный момент достаточно увидеть, что мы заложили основу для запуска определенного действия. Теперь, когда мы создали соглашение, мы можем легко добавлять новые действия определенным и разумным способом.

Ползать с PhantomJS используя всадника

Horseman — это пакет Node.js, который предоставляет мощный интерфейс для создания и взаимодействия с процессами PhantomJS. Всестороннее объяснение Horseman и его функций оправдывает свою собственную статью, но достаточно сказать, что оно позволяет легко имитировать практически любое поведение, которое пользователь-человек может демонстрировать в своем браузере. Horseman предоставляет широкий спектр параметров конфигурации, включая такие, как автоматическое внедрение jQuery и игнорирование предупреждений SSL-сертификатов. Он также предоставляет функции для обработки файлов cookie и создания снимков экрана.

Каждый раз, когда мы запускаем действие через нашу среду CLI, наш скрипт входа ( run.js ) создает экземпляр Horseman и передает его указанному модулю действия. В псевдокоде это выглядит примерно так:

 var phantomInstance = new Horseman({ phantomPath: '/usr/local/bin/phantomjs', loadImages: true, injectJquery: true, webSecurity: true, ignoreSSLErrors: true }); performAction(phantomInstance, ...); 

Теперь, когда мы запускаем нашу команду, экземпляр Horseman и входной URL-адрес передаются в модуль hello_world , в результате чего PhantomJS запрашивает URL-адрес, захватывает его код состояния и выводит его на консоль. Мы только что запустили наш первый добросовестный обход с помощью Всадника. Легкомысленный до!

Вывод запущенной команды hello_world

Методы Всадника с цепочкой для сложных взаимодействий

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

Обратите внимание: этот пример предназначен исключительно для демонстрационных целей и не должен рассматриваться как жизнеспособный метод создания репозиториев Github. Это всего лишь пример того, как можно использовать Horseman для взаимодействия с веб-приложением. Вам следует использовать официальный Github API, если вы заинтересованы в создании репозиториев в автоматическом режиме.

Предположим, что новое сканирование будет запущено следующим образом:

 $ node run.js -x create_repo 

В соответствии с соглашением инфраструктуры CLI, которое мы уже создали, нам нужно добавить новый модуль в каталог действий с именем create_repo.js . Как и в нашем предыдущем примере «hello world», модуль create_repo экспортирует одну функцию, содержащую всю логику для этого действия.

 module.exports = function (phantomInstance, username, password, repository) { if (!username || !password || !repository) { throw 'You must specify login credentials and a repository name'; } ... } 

Обратите внимание, что с помощью этого действия мы передаем в экспортируемую функцию больше параметров, чем мы делали ранее. Параметры включают в себя username , password и repository . Мы передадим эти значения из run.js только пользователь успешно выполнит запрос подсказки.

Прежде чем что-либо из этого может произойти, мы должны добавить логику в run.js чтобы вызвать приглашение и захватить данные. Мы делаем это, добавляя регистр в наш главный оператор switch :

 switch (program.actionToPerform) { case 'create_repo': prompt.get([{ name: 'repository', description: 'Enter repository name', required: true }, { name: 'username', description: 'Enter GitHub username', required: true }, { name: 'password', description: 'Enter GitHub password', hidden: true, required: true }], function (err, result) { performAction( phantomInstance, result.username, result.password, result.repository ); }); break; ... 

Теперь, когда мы добавили этот хук в run.js , когда пользователь вводит соответствующие данные, он будет передан действию, что позволит нам продолжить сканирование.

Что касается самой create_repo сканирования create_repo , мы используем массив методов Horseman, чтобы перейти на страницу входа в Github, ввести предоставленные username и password и отправить форму:

 phantomInstance .open('https://github.com/login') .type('input[name="login"]', username) .type('input[name="password"]', password) .click('input[name="commit"]') 

Мы продолжаем цепочку, ожидая загрузки страницы отправки формы:

 .waitForNextPage() 

после чего мы используем jQuery для определения успешности входа в систему:

 .evaluate(function () { $ = window.$ || window.jQuery; var fullHtml = $('body').html(); return !fullHtml.match(/Incorrect username or password/); }) .then(function (isLoggedIn) { if (!isLoggedIn) { throw 'Login failed'; } }) 

В случае сбоя входа в систему выдается ошибка. В противном случае мы продолжаем цепочку методов, чтобы перейти на страницу нашего профиля:

 .click('a:contains("Your profile")') .waitForNextPage() 

Как только мы попадаем на страницу нашего профиля, мы переходим на нашу вкладку репозиториев:

 .click('nav[role="navigation"] a:nth-child(2)') .waitForSelector('a.new-repo') 

Находясь на нашей вкладке репозитории, мы проверяем, существует ли репозиторий с указанным именем. Если это так, то мы выдаем ошибку. Если нет, то мы продолжим нашу последовательность:

 // Gather the names of the user's existing repositories .evaluate(function () { $ = window.$ || window.jQuery; var possibleRepositories = []; $('.repo-list-item h3 a').each(function (i, el) { possibleRepositories.push($(el).text().replace(/^\s+/, '')); }); return possibleRepositories; }) // Determine if the specified repository already exists .then(function (possibleRepositories) { if (possibleRepositories.indexOf(repository) > -1) { throw 'Repository already exists: ' + repository; } }) 

Предполагая, что никаких ошибок не было, мы продолжаем, программно нажимая кнопку «новый репозиторий» и ожидая следующую страницу:

 .click('a:contains("New")') .waitForNextPage() 

после чего мы вводим предоставленное имя repository и отправляем форму:

 .type('input#repository_name', repository) .click('button:contains("Create repository")') 

Как только мы достигаем полученной страницы, мы знаем, что хранилище было создано:

 .waitForNextPage() .then(function () { console.log('Success! You should now have a new repository at: ', 'https://github.com/' + username + '/' + repository); }) 

Как и при любом сканировании всадника, очень важно, чтобы мы закрыли экземпляр Horseman в конце:

 .close(); 

Если не закрыть экземпляр Horseman, это может привести к тому, что процессы PhantomJS, оставшиеся без внимания, будут сохраняться на компьютере.

Сканирование для сбора данных

На данный момент мы собрали статическую последовательность действий для программного создания нового репозитория на GitHub. Чтобы достичь этого, мы приковали цепью ряд методов Всадника.

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

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

В этом разделе демонстрируется пример извлечения основных данных со страницы (в данном случае якорных ссылок). Один из сценариев, в котором это может быть необходимо, — это создание сканера обнаружения порчи для каждого URL в домене.

Как и в нашем последнем примере, мы должны сначала добавить новый модуль в каталог действий:

 module.exports = function (phantomInstance, url) { if (!url || typeof url !== 'string') { throw 'You must specify a url to gather links'; } phantomInstance .open(url) // Interact with the page. This code is run in the browser. .evaluate(function () { $ = window.$ || window.jQuery; // Return a single result object with properties for // whatever intelligence you want to derive from the page var result = { links: [] }; if ($) { $('a').each(function (i, el) { var href = $(el).attr('href'); if (href) { if (!href.match(/^(#|javascript|mailto)/) && result.links.indexOf(href) === -1) { result.links.push(href); } } }); } // jQuery should be present, but if it's not, then collect the links using pure javascript else { var links = document.getElementsByTagName('a'); for (var i = 0; i < links.length; i++) { var href = links[i].href; if (href) { if (!href.match(/^(#|javascript|mailto)/) && result.links.indexOf(href) === -1) { result.links.push(href); } } } } return result; }) .then(function (result) { console.log('Success! Here are the derived links: \n', result.links); }) .catch(function (err) { console.log('Error getting links: ', err); }) // Always close the Horseman instance // Otherwise you might end up with orphaned phantom processes .close(); 

А затем добавьте хук для нового действия в run.js :

 switch (program.actionToPerform) { ... case 'get_links': prompt.get([{ name: 'url', description: 'Enter URL to gather links from', required: true, conform: function (value) { return validUrl.isWebUri(value); } }], function (err, result) { performAction(phantomInstance, result.url); }); break; 

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

 $ node run.js -x get_links 

Это действие демонстрирует извлечение данных со страницы и не использует какие-либо действия браузера, встроенные в Horseman. Он напрямую выполняет любой JavaScript, который вы поместили в evaluate() , и делает это так, как если бы он был изначально запущен в среде браузера.

В этом разделе следует отметить одну последнюю вещь, о которой упоминалось ранее: вы можете не только выполнять пользовательский JavaScript-код в браузере с помощью evaluate() , но вы также можете вводить внешние сценарии в среду выполнения до запуска логики оценки. , Это можно сделать так:

 phantomInstance .open(url) .injectJs('scripts/CustomLogic.js') .evaluate(function() { var x = CustomLogic.getX(); // Assumes variable 'CustomLogic' was loaded by scripts/custom_logic.js console.log('Retrieved x using CustomLogic: ', x); }) 

Расширив логику выше, вы можете выполнять практически любые действия на любом сайте.

Используя всадника, чтобы сделать скриншоты

Последний пример использования, который я хочу продемонстрировать, заключается в том, как использовать Horseman для создания снимков экрана. Мы можем сделать это с помощью метода screenshotBase64 () Horseman, который возвращает закодированную в base64 строку, представляющую снимок экрана.

Как и в нашем предыдущем примере, мы должны сначала добавить новый модуль в каталог действий:

 module.exports = function (phantomInstance, url) { if (!url || typeof url !== 'string') { throw 'You must specify a url to take a screenshot'; } console.log('Taking screenshot of: ', url); phantomInstance .open(url) // Optionally, determine the status of the response .status() .then(function (statusCode) { console.log('HTTP status code: ', statusCode); if (Number(statusCode) >= 400) { throw 'Page failed with status: ' + statusCode; } }) // Take the screenshot .screenshotBase64('PNG') // Save the screenshot to a file .then(function (screenshotBase64) { // Name the file based on a sha1 hash of the url var urlSha1 = crypto.createHash('sha1').update(url).digest('hex') , filePath = 'screenshots/' + urlSha1 + '.base64.png.txt'; fs.writeFile(filePath, screenshotBase64, function (err) { if (err) { throw err; } console.log('Success! You should now have a new screenshot at: ', filePath); }); }) .catch(function (err) { console.log('Error taking screenshot: ', err); }) // Always close the Horseman instance // Otherwise you might end up with orphaned phantom processes .close(); }; 

А затем добавьте хук для нового действия в run.js :

 case 'take_screenshot': prompt.get([{ name: 'url', description: 'Enter URL to take screenshot of', required: true, conform: function (value) { return validUrl.isWebUri(value); } }], function (err, result) { performAction(phantomInstance, result.url); }); break; 

Теперь вы можете делать скриншоты с помощью следующей команды:

 $ node run.js -x take_screenshot 

Причина использования строк в кодировке base64 (а не, например, сохранения реальных изображений) заключается в том, что они являются удобным способом представления необработанных данных изображения. Этот ответ StackOverflow будет более детальным.

Если вы хотите сохранить реальные изображения, вы должны использовать метод screenshot () .

Вывод

В этом руководстве была предпринята попытка продемонстрировать как пользовательскую микрофрейму CLI, так и некоторую базовую логику для сканирования в Node.js, используя пакет Horseman для использования PhantomJS. Хотя использование среды CLI, вероятно, принесет пользу многим проектам, использование обхода обычно ограничивается весьма специфическими проблемными областями. Одной из общих областей является обеспечение качества (QA), где сканирование может использоваться для функционального тестирования и тестирования пользовательского интерфейса. Другая область — это безопасность, где, например, вы можете периодически сканировать свой веб-сайт, чтобы определить, не был ли он поврежден или иным образом скомпрометирован.

Каким бы ни был случай для вашего проекта, убедитесь, что четко определили свои цели и будьте максимально ненавязчивы. Получите разрешение, когда можете, будьте вежливы в максимально возможной степени и старайтесь никогда не делать DDoS сайт. Если вы подозреваете, что генерируете много автоматизированного трафика, то вам, вероятно, следует пересмотреть ваши цели, реализацию или уровень разрешений.