Pull to refresh

На-click-ать известность, или как взбудоражить робота и … остальных

Reading time22 min
Views6.3K


Давным-давно, у фасада далекого-далекого магазина состоялся подслушанный разговор:
NB: - А как привести много посетителей на свой новый сайт?

GURU: - Ну можно ссылок «раскидать» на разных форумах и в соц. сетях. Поисковая оптимизация поможет и контент. Можно тизерные сети привлечь, а можно много раз посетить сайт через разные прокси ...

NB: - И чем же помогут такие посещения, ведь это иллюзия живых людей?

GURU: - Счетчик статистики от google или от yandex объяснит поисковикам, что сайт становится популярным. Да еще и реферер можно связать с посещаемыми сайтами по запросам. Подрастет позиция в поисковиках, а значит и подрастет поисковый трафик.

NB: - А где же взять такое количество прокси?

GURU: - Где?… Ну в интернете поищи...
NB перестал спрашивать, видимо, опасаясь раздражать явно более опытного собеседника.
GURU закатил глаза, как бы подчеркивая исчерпанность темы про прокси и замолчал…

Разумеется, GURU знал, что поисковый запрос (например по слову: прокси) очень скоро приведет NB к получению желаемого списка «адрес: порт». Но после первых экспериментов NB, так же быстро, поймет:

  • Не все адреса в его списке рабочие;
  • Не все прокси одинаково хороши;
  • «Накликивать» сайт через прокси вручную — задача, требующая значительной воли;
  • «Не правильный» прокси навредит ситуации, т.к. сайт может быть заподозрен скриптами гигантов в накрутке.


В этом материале я расскажу о том, как подручными (а главное универсальными) средствами
(без использования специфического проприетарного ПО, такого как ZennoPoster и пр… )
построить автоматизированный инструмент по получению списка «действительно годных» прокси и использованию их для организации (автоматизированного) посещения продвигаемого
сайта при помощи браузера Chrome.

Следуя инструкции, вы получите готовый инструмент, который позволит:

  • «click»-ать (посещать) целевой сайт полностью автоматически, не опасаясь компроментации;
  • полностью эмулировать поведение пользователя;
  • организовать все посещения по расписанию (сценарию);
  • делать все вышеописанное в необходимом для продвижения количестве раз.

И хотя вся моя работа (вместе с исследованиями) заняла неделю, вам не потребуется и двух дней, чтобы построить такой инструмент, обладая базовыми знаниями о командной строке, PHP и JavaScript.


Однако, до того как вы пролистаете ниже следующей схемы, скажу пару слов о том, для чего и для кого подготовлен этот материал.

Материал будет полезен в том случае, если вы хотите разобраться как устроены конструкции (? или конструкторы?) при помощи которых можно относительно быстро построить приложение, легко и без затрат адаптируемое к изменению нагрузки. Если вас интересует возможность построения приложения на основе сервисной шины (ESB).

Текст будет полезен, если вы хотели поближе познакомиться с применением Docker для мгновенного построения систем. Или же если вам просто интересен Selenium Server и нюансы получения контента/проявления HTTPx активности.
Для «использования сразу», вдумчиво читать все это не стоит. Код уж точно.
Сразу перейти к настройке готового инструмента. Настройка займет менее 20 минут.
Инструкция предполагает, что у вас в распоряжении 2 машины с установленной Ubuntu 18.04.
Одна для инфраструктуры (docker), другая для управления процессом (process).

Предполагается, что на docker уже установлены следующие пакеты:
git, docker, docker-compose

Предполагается, что на process уже установлены следующие пакеты:
git, php-common, php-cli, php-curl, php-zip, php-memcached, composer
Если на этом месте у вас возникли вопросы, предлагаю затратить 15 минут на прочтение всего материала полностью.

docker

# Все действия предполагают root-привилегии.
# Предполагается, что не заняты следующие TCP-порты:
#     11300, 11211, 4444, 5930, 8080, 8081, 8082, 8083

# Клонировать репозиторий с инфраструктурой в домашний каталог
# "не root-пользователя" и запустить контейнеры

git clone \
https://oauth2:YRGzV8Ktx2ztoZg_oZZL@git.ituse.ru/deploy/esb-infrastructure.git

cd esb-infrastructure
docker-compose up --build -d

# Вам потребуется чашка кофе и 3 минуты терпения после окончания
# процесса старта контейнеров.
# Примерно столько времени необходимо на развертывания web-панелей.

process

# Все действия предполагают привелегии обыкновенного пользователя.

# Клонировать репозиторий с проектом на process-машину в любой каталог,
# доступный для записи и до-установить необходимые php-пакеты

git clone \
 https://oauth2:YRGzV8Ktx2ztoZg_oZZL@git.ituse.ru/deploy/clicker-noserver.git

cd clicker-noserver
composer update

# Сконфигурировать приложение. Для это в файле заменить строки "XXXXXXXX"

mv app/settings.php.dist app/settings.php

# Запустить обработчики очередей.

gnome-terminal \
--tab -e 'bash -c "php app/src/Process/noserver/singleProcess.php curl"' \
--tab -e 'bash -c "php app/src/Process/noserver/singleProcess.php timezone"' \
--tab -e 'bash -c "php app/src/Process/noserver/singleProcess.php whoer"'

# Дать задание. Предполагается, что файл со списком прокси,
# формата адрес:порт - log/list.proxy

php app/src/Utils/givethejob.php ./log/list.proxy

Ждать, наблюдая за происходящим через web-панель (http://ip-адрес-докер-машины:8080).
Результат будет доступен в очереди located.

Дробление и планирование




На схеме, приведенной выше, описана последовательность шагов процесса, ожидаемый результат для каждого шага и ресурсы, которые потребуются для создания каждой из частей (подробности: Задача 2, Задача 3, Задача 4,5,6).

В моем случае все происходит на двух машинах Ubuntu 18.04. Одна из них управляет процессом. На другой запущены несколько Docker-контейнеров инфраструктуры.

Отговорки и отказы


Весь код пакета состоит из трех частей.
Одна из них — не моя (отмечу, что это аккуратный и красивый код). Источник этого кода packagist.org.

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

Остальное — «нелегкое историческое наследие». Эта часть кода создавалась на протяжении довольно длительного времени. В том числе в тот период, когда я еще не имел большой сноровки в программировании.

Именно в этом кроется причина расположения репозиториев на моем GitLab и пакетов на моем Satis. Для публикации на GitHub.com и packagist.org этот код потребует обработки и более тщательного документирования.

Все части кода открыты для неограниченного использования. Репозитории и пакеты будут доступны «вечно».

Однако, при ре-публикации кода, я буду признателен вам за размещение ссылки на меня либо на эту статью.

Немного об архитектуре


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

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

Решение, выполненное по такому принципу, позволяет менять количество запущенных обработчиков задач (воркеров) для каждого из шагов процесса. Разное количество воркеров для каждого шага приведет к «0» времени простоя шагов последующих из-за длительности обработки задач шагов предыдущих.

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

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

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

Очередь обмена сообщениями. MQ и ESB


В качестве нижнего уровня (MQ) мы будем использовать beanstalkd. Маленький, легкий, не требующий конфигурации, доступный в deb-пакете и в Docker-контейнере, незаметный труженик. Логический же уровень (ESB), будет осуществлять код на PHP.

Для реализации будут использованы два класса. esbTask и nextStepWorker.

esbTask

class esbTask   // Вагонетка с данными, несущаяся по рельсам процесса
{
  // immutable-класс;

  // "Конверт" (обертка на основе ESB-подходов), содержащий payload
  // Сериализуется и десериализуется
  // Реализует различные функции конфигурации "конверта"
  ....
}

Экземпляр этого класса служит для «адресации» paylod'а сквозь шаги процесса. В концепции ESB применены несколько принципов/паттернов. На два из них стоит обратить внимание отдельно:

  • О пути (последовательности шагов) процесса, в каждый момент времени реализации процесса,
    не знает никто кроме передаваемого конверта;
  • Каждый конверт имеет три возможных направления исхода:
    • следующий шаг процесса (нормальное продолжение);
    • шаг остановки (мишень остановки — выбирается следующим шагом, в случае отсутствия смысла в продолжении процесса/stop-ситуации);
    • шаг ошибки (мишень аварийного завершения — выбирается следующим шагом, в случае ошибки воркера).

В очереди объект представлен в json, спрятанном здесь...
// json-представление esbTask
{
    // Идентификатор десериализуемого класса (один из потомков esbTask)
    "_type":"App\\\\rebean\\\\payloads\\\\ESBtaskQueue",

    // Адрес
    "task":"task:queue@XXX.XXX.XXX.XXX:11300",

    // Следующие шаги процесса (опционально)
    "replyto":[
        "othertask1:nextqueue1@yyy.XXX.XXX.XXX:11300",
        "othertask2:nextqueue2",
        "othertask3:nextqueue3",
    ],

    // Мишени для ошибок (опционально)
    "onerror":[
        "error:errorsstep@zzz.XXX.XXX.XXX:11300",
        "error:errorsstep1",
        "error:errorsstep2",
        "error:errorsstep3"
    ],

    // Мишени для стоп случаев (опционально)
    "onstop":[
        "stop:stopstep@kkk.XXX.XXX.XXX:11300",
        "stop:stopstep1",
        "stop:stopstep2",
        "stop:stopstep3"
    ],

    // Передаваемые данные
    "payload":{
        ....
    },

    // Годен до ... (опционально)
    "till":[
        ....
    ],

    // Доступен для обработки с ... (опционально)
    // Пакет будет доступен для обработки начиная с
    // такой-то секунды (LINUX-TimeStamp)
    "since":[
        1540073089.8833,
    ],

    // Кол-во сделанных шагов по процессу
    "points":1,

    // Идентификатор группы. Используется для различных целей
    "groupid":""
}


nextStepWorker

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

class nextStepWorker extends workerConstructor
{
    // Класс является базовым для создания воркеров

    // Десериализует строку в объект esbTask
    // Конфигурирует логику воркера (рутинную)
    // Инициализирует работу с MQ-уровнем (beanstalkd)
    // Инициализирует работу с кешем (Memcached)
    // Инициализирует работу с БД (MySQL)
    // Реализует:
        - логику (в базе - пустую);
        - обработку ошибок и stop-ситуаций;
        - информирование на каждом этапе выполнения (log, event, mq)
    ....
}

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

Решение каждой из задач сводится к тому, чтобы:

  1. Получить esbTask и запустить воркер;
  2. Реализовать логику, сохранив результаты в payload;
  3. Завершить выполнение воркера (аварийно или нормально — не важно).

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

Делай раз. Проверить доступность


Фактически, создание воркера для решения любой из задач, есть реализация одного метода. Пример (упрощенный) реализации воркера, решающего Задачу 2 выглядит следующим образом:

// app/src/Process/worker/curlChecker.php
....
class curlChecker extends nextStepWorker
{
    const PROXY_INFO = 'https://api.ipfy.me?format=json&geo=true';
    const PROXY_TIMEOUT = '40';
    const COMMAND = "curl -m %s -Lx  http://%s:%s '%s'";

    public function logic()
    {
        // Извлекаем разные нужные переменные. В т.ч. и из payload
        extract($this->context());

        // Устанавливаем defaults для отсутствующих в payload переменных
        $curltimeout = $curltimeout ?? self::PROXY_TIMEOUT;
        $curlchecker = $curlchecker ?? self::PROXY_INFO;

        // Формируем и выполняем команду
        $line = sprintf(
            static::COMMAND, $curltimeout, $host, $port, $curlchecker
        );
        exec($line, $info);

        // Преобразуем и проверяем работоспособность прокси 
        // (если пусто, то stop-исключение)
        $info = arrays::valid_json(implode('', info));
        if (empty($info))
            throw new \Exception("Bad proxy: $host:$port!", static::STATUS_STOP);

        // Добавляем результат в payload
        $this->enrich(['info'])
            ->sets(compact('info'));
    }
}

Несколько строк кода и все сделано и «откомандировано» в следующий этап.

Определить TimeZone. TimeZoneDB и для чего это нужно...


Глубокое тестирование входящих запросов включает в себя сопоставление времени окна браузера со временем, в котором существует IP-адрес-источник-запроса.

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

Для выяснения времени, возьмем широту и долготу из результатов предыдущего шага процесса и получим данные о часовом поясе, в котором будет работать наш будущий экземпляр окна браузера. Эти данные нам предоставят профессионалы в области времени.

Упрощенный воркер для решения этой задачи (Задача 3) будет полностью аналогичен предыдущему. Различие лишь в URL запроса. Полную версию вы сможете найти в файле:
// app/src/Process/worker/timeZone.php

Немного об инфраструктуре


Кроме описанного beanstalkd, нашему инструменту понадобятся:

  • Memcached — для задач кэширования;
  • Selenium Server — удобно запускать Selenium Web Driver в отдельном контейнере и можно наблюдать за процессом по VNC;
  • Панели для наблюдения за beanstald, Memcached и VNC.

Для быстрого развертывания всего этого очень удобен Docker (Как установить на Ubuntu).

И "оркестратор" для него - docker-compose (команды для установки)...
sudo apt-get -y update
sudo apt-get -y install docker-compose


Эти инструменты позволяют запускать уже скомпонованные и настроенные (кем-то ранее) сервера/процессы в отдельных «контейнерах» материнской ОС. За подробностями рекомендую обратиться к этой либо к этой статьям.

Итак…

Для запуска инфраструктуры вам нужно несколько команд в консоле:

# В консоле машины, предназначенной для запуска docker-контейнеров
# находясь в домашнем каталоге (для оркестратора это важно)
# действуя под чарами sudo -s

git clone \
 https://oauth2:YRGzV8Ktx2ztoZg_oZZL@git.ituse.ru/deploy/esb-infrastructure.git \
 panels
cd panels
docker-compose up --build -d

# потребуется чашечка кофе.

В результате успешного выполнения команды на машине с адресом XXX.XXX.XXX.XXX,
вы получите следующий набор сервисов:
— XXX.XXX.XXX.XXX:11300 — beanstalkd
— XXX.XXX.XXX.XXX:11211 — Memcached
— XXX.XXX.XXX.XXX:4444 — Selenium Server
— XXX.XXX.XXX.XXX:5930 — VNC-сервер для контроля того что происходит в Chrome
— XXX.XXX.XXX.XXX:8081 — Веб-панель для общения с Memcached (admin:pass)
— XXX.XXX.XXX.XXX:8082 — Веб-панель для общения с beanstalkd
— XXX.XXX.XXX.XXX:8083 — Веб-панель для общения с VNC (пароль:secret)
— XXX.XXX.XXX.XXX:8080 — Общая веб-панель

"Посмотреть, все ли на месте", "попасть в консоль к контейнеру", "остановить инфраструктуру" можно командами в спойлере.
# В консоле машины, предназначенной для запуска docker-контейнеров,
# находясь в каталоге ..../panels/


# Посмотреть запущенные контейнеры

docker-compose ps

# Name                 Command               State                                 Ports
# ------------------------------------------------------------------------------------------------------------------------
# beanstalkd   /usr/bin/beanstalkd              Up      0.0.0.0:11300->11300/tcp
# chrome       start-cron                       Up      0.0.0.0:4444->4444/tcp, 0.0.0.0:5930->5900/tcp
# memcached    docker-entrypoint.sh memcached   Up      0.0.0.0:11211->11211/tcp
# nginx        docker-php-entrypoint /sta ...   Up      0.0.0.0:8443->443/tcp, 0.0.0.0:8080->80/tcp,
#                                                       0.0.0.0:8082->8082/tcp, 0.0.0.0:8083->8083/tcp, 9000/tcp
# vnc          /usr/bin/supervisord -c /e ...   Up      0.0.0.0:8081->8081/tcp


# Попасть в консоль машины с именем chrome

docker exec -ti chrome /bin/bash


# Остановить все контейнеры и почистить мусор

docker-compose stop && docker rm $(docker ps -a -q)

Задачи 4,5,6 — объединяем в одну утилиту


Подробно посмотрев дробление на задачи (схема выше), легко убедиться, что из оставшихся задач только одна (задача 6) зависит от внешнего ресурса. Выполняя задачи с «условно гарантированным» временем выполнения (не зависящие от не контролируемых факторов), мы не получим дополнительных плюсов к скорости всего процесса. В этой связи эти задачи (4,5,6) и были объединены в один воркер. Файл воркера называется:
// app/src/Process/worker/whoerChecker.php

Сделать настройки для Chrome. Плагины


Chrome гибко конфигурируется при помощи плагинов.

Плагин для Chrome это архив, который содержит файл manifest.json. Он и описывает плагин. Архив, также, содержит набор JavaScript, html, css и др.-файлов, необходимых плагину (подробности).

В нашем случае, один из JavaScript-файлов будет выполнен в контексте рабочего окна Chrome и все необходимые настройки вступят в силу.

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

Фрагмент кода, который делает архив:

//  app/src/Chrome/proxyHelper.php
....
class proxyHelper extends sshDocker{
    ....
    // $name - имя архива-плагина
    // $files - [ ... 'имя файла внутри архива-плагина' => 'содержимое', ...]
    protected function buildPlugin(string $name, array $files)
    {
        $this->last = "$this->cache/$name";
        if (!file_exists("$this->last")) {
            $zip = new \ZipArchive();
            $zip->open("$this->last", \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
            foreach ($files as $n => $data) {
                $zip->addFromString(basename($n), $data);
            }
            $zip->close();
        }
        $this->all[] = $this->last;
        $this->all = array_unique($this->all);

        return $this;
    }
    ....
}

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

Смена времени окна


Для смены глобального времени запущенного экземпляра Chrome нам нужно заменить window.Date на класс с аналогичным функционалом, но действующий в нужном часовом поясе.

Я очень признателен за труд Sampo Juustila. Скрипт был сделан для автоматизированного тестирования UI, но после небольшой доработки был применен.

Здесь есть нюанс, на который я хочу обратить ваше внимание. Связан он с контекстом выполнения скриптов, описанных в manifest.json.

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

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

Решение представлено следующим фрагментом кода:

// app/chromePlugins/timeShift/content.js

// Создали элемент на странице
var s = document.createElement('script');

// Загрузили файл с основным скриптом из плагина
s.src = chrome.extension.getURL('timeshift.js');

// Вставили текст в созданный элемент
(document.head || document.documentElement).appendChild(s);

Настройка прокси


Настройка прокси в Chrome настолько проста, что я скрою js-код в спойлер
// app/chromePlugins/proxy/background.js

var config = {
    mode: "fixed_servers",
    rules: {
        singleProxy: {
            scheme: "%scheme",
            host: "%proxy_host",
            port: parseInt(%proxy_port)
        },
        bypassList: ["foobar.com"]
    }
};

chrome.proxy.settings.set({value: config, scope: "regular"}, function () {
});

function callbackFn(details) {
    return {
        authCredentials: {
            username: "%username",
            password: "%password"
        }
    };
}

chrome.webRequest.onAuthRequired.addListener(
    callbackFn,
    {urls: ["&gtall_urls&lt"]},
    ['blocking']
);


Путь плагинов к Chrome


Все плагины именуются по схеме и складываются во временную папку машины, управляющей процессом.
// схема именования для proxy-плагина:
proxy-[адрес]-[порт]-[протокол]>.zip
timeshift-["-"|""]-[сдвиг_в_минутах_от_GMT].zip

Далее нам необходимо установить эти плагины в docker-контейнер, который запущен на машине, отвечающей за инфраструктуру.

Мы будем делать это при помощи ssh. Для этого я познакомился с phpseclib (хоть позднее и пожалел об этом). Увлеченный необычным поведением библиотеки, я истратил день на ее изучение.

Консольный клиент ssh здесь подойдет лучше и будет работать быстрее, но дело уже было сделано.

За низкий уровень (работа с SFTP и SSH) отвечает базовый класс (ниже). Замена этого класса позволит заменить phpseclib на консольный клиент.

// app/src/Chrome/sshDocker.php

// Класс привязан константами (DOCKER_HOST, DOCKER_USER, DOCKER_PASS)
// к глобальной конфигурации: app/settings.php
// Ниже два наиболее значимых метода
....
class sshDocker
{
    ....
    // Глобальная константа. Содержит путь к исполняемому docker
    // Путь может быть разным. Зависит и от дистрибутива и от способа установки
    // Конфигурируется: app/techs.php
    const EXEC_DOCKER = DOCKER_BIN_PATH . "/docker exec -i %s %s";
    ....

    // Позволяет выполнить команду под sudo на хост машине (не в контейнере), если DOCKER_USER - судоер
    protected function sudo(string $command, string $expect = '.*'){...}

    // Позволяет выполнить команду в Docker-контейнере, для которого создан экземпляр объекта
    // Команда выполняется за счет запуска на хост-машине команды по шаблону self::EXEC_DOCKER
    protected function execDocker(string $command, string $expect){...}
    ....
}

Порожденный от базового sshDocker и уже нам известный класс proxyHelper не только производит плагины, но и кладет их во временную папку контейнера инфраструктуры.

// app/src/Chrome/proxyHelper.php
....
class proxyHelper extends sshDocker
{
    ....

    public static function new(string $docker, $plugins)
    {
        return (new self($docker, $plugins))
            ->setupPlugins();
    }
    ....
}

Запустить Chome с настройками


Запустить настроенный Chrome нам поможет Selenium Server.

Selenium Server — фрейм-ворк, созданный командой FaceBook специально для тестирования WEB-интерфейсов.

Фрейм-ворк позволяет разработчику программно эмулировать любое действие пользователя в окне браузера (используется Chrome либо Firefox).

Selenium Server адаптирован к использованию со многими языками и де-факто является стандартным инструментом для написания тестовых сценариев.

Наилучший способ получить свежий релиз для использования в проекте:

composer require facebook/webdriver

Традиционное конфигурирование основного экземпляра объекта Selenium Server (RemoteWebDriver) показалось мне многословным.
// URL-ЦЕЛь
$url = "https://example.com/books/196/empire-v-povest-o-nastoyashem-sverhcheloveke";

// URL, по которому доступна инфраструктура (Selenium Server)
$server = 'http://' . DOCKER_HOST . '/wd/hub';

// экземпляр объекта опция, которым мы запрещаем показывать нотификации
$options = new ChromeOptions();
$options->addArguments(array( '--disable-notifications' ));

// Установка опций в конф-структуру
$capabilities = DesiredCapabilities::chrome();
$capabilities->setCapability(ChromeOptions::CAPABILITY, $options);

// Получение экземпляра окна с отстрелом по таймауту 5000 мс и загрузка целевой URL
$driver = RemoteWebDriver::create($server, $capabilities, 5000);
$page = $driver->get($url);


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

// app/src/Process/worker/whoerChecker.php

....
class whoerChecker extends nextStepWorker
{
    // Настройки из глобальной конфигурации: app/settings.php

    // URL Selenium Server
    const SELENIUM_SERVER = CHVM;
    // Имя докер-контейнера
    const DOCKER_NAME = DOCKER_NAME;
    ....

    public function config()
        ....

        // Полный аналог со стандартным получением экземпляра окна браузера:
        // $driver = RemoteWebDriver::create($server, $capabilities, 5000);

        $chrome = Chrome::driver(
                static::SELENIUM_SERVER, Chrome::capabilities(static::DOCKER_NAME, $plugins), 5000
        );
        ....
    }
    ....

Глаз сразу цепляется за $plugins. $plugins — это структура данных, отвечающая за конфигурирование плагинов. За директорию каждого и за замещение плейсхолдеров в JavaScript файлах плагина.

Cтруктура описана в файле app/plugs.php и является частью глобальных опций app/settings.php.
// app/plugs.php

const PLUGS = [
    'timeshift' => [
        'path' => PROJECTPATH . '/app/chromePlugins/timeShift',
        'files' => ['manifest.json', 'timeshift.js', 'content.js'],
        'fields' => ['%addsminutes' => 'timeshift']
    ],
    'proxy' => [
        'path' => PROJECTPATH . '/app/chromePlugins/proxy',
        'files' => ['manifest.json', 'background.js'],
        'fields' => [
            '%proxy_host' => 'host', '%proxy_port' => 'port', '%scheme' => 'scheme',
            '%username' => 'user', '%password' => 'pass'
        ]
    ]
];


Парсинг страницы с Selenium WebDriver — очень прост.

....
$url = 'https://адрес_страницы_источника/и_какой-нибудь_путь';
$page = $chrome->get($url);

....
// строка с xPath-адресацией нужного элемента
$xpath = '/html[1]/body[1]/div[1]/div[1]/div[1]/div[2]/div[1]/div[1]/div[1]/div[1]/strong[1]';
$element = page->findElement(WebDriverBy::xpath($xpath));

....
// Текстовое воплощение
$text = $element->getText();
// HTML-воплощение
$html = $element->>getAttribute('innerHTML');

....

Как я уже писал, все эти действия реализуются утилитой третьего шага (Задача 4,5,6):
// app/src/Process/worker/whoerChecker.php
Завершая описание работы с Selenium Server, хочу обратить ваше внимание на то, что при использовании этой технологии в промышленных масштабах (1000 — 3000 открываний страниц), нередки ситуации, когда сессия с Selenium Server завершается некорректно. Окно оказывается бесхозным. И таких окон может накопиться очень много.

Способов борьбы с «брошками» рассматривалось несколько. Работа «съела» 2 дня. Самым эффективным оказался cron. Корректная его установка и настройка в Docker-контейнере превратилась в отдельную задачу, заботливо и очень подробно описанной renskiy, в статье, посвященной ТОЛЬКО ЭТОЙ ТЕМЕ (чему я был удивлен).

Автоматическая пересборка исходного Docker-имиджа и встройка нескольких скриптов по закрытию брошек и очистке от неиспользуемых плагинов описана в docker-compose.yml, репозитория инфраструктуры. Периодичность очистки задается в файле killcron, того же репозитория.

WebRTC


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

Помимо разницы во времени (браузер и IP адрес), существуют еще два источника деанонимзации «сидящего за прокси». Это flash и WebRTC технологии, встроенные в браузер. В нашем браузере Flash отключен, WebRTC — нет.

Причина обеих возможностей провала одна — вездесущие и юркие UDP-пакеты. Для WebRTC это два порта: 3478 и 19302.

Для прекращения исхода «лазутчиков» из контейнера «chrome», на хост-машине с контейнерами инфраструктуры применяется правило iptables:

iptables -t raw -I PREROUTING -p udp -m multiport --dports 3478,19302 -j DROP

Реализует эту задачу все тот же proxyHelper.

Остальные воркеры


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

Он будет усеченной версией whoerChecker. Думаю сделать это самостоятельно, используя все написанное, не составит труда.

Результат работы всего процесса, попадающий в очередь located, содержит данные о «степени» анонимности каждого прошедшего проверку адреса прокси сервера.

При «игре» против счетчиков главное помнить об анонимности и не увлечься роботизированными посещениями. Соблюдение принципа «не увлекайся кликами» обеспечено возможностью организации действий по расписанию, которая заложена в esbTask (поле since нашего ESB конверта).

Если постараться и сделать все аккуратно, то yandex-метрика целевого сайта будет похожа на рисунок ниже.



Как собрать все вместе


Итак, дано:

  • утилиты, которые способны принять «на вход» (в качестве аргумента командной строки) esbTask json-строковом виде и выполнить некоторую логику, а результаты отправить в beanstalkd;
  • очередь сообщений (MQ), на базе beanstalkd;
  • Linux-машина (Process-машина);

При таком «Дано», обычно, я применяю libevent и React PHP. Все это, дополненное несколькими инструментами, позволяет управлять количеством (в заданных пределах) экземпляров обработчиков для каждого этапа процесса автоматически.

Однако, учитывая размер статьи и специфику темы, я буду рад описать все это в отдельном материале. Эта статья — технология "noserver". Будущий материал — "server".
Дата его публикации связана с Вашим интересом, Уважаемый Читатель.
Для меня этот интерес очень важен. Его можно выразить количественно и устроить, например,… голосование. И, конечно же, есть такие численные показатели, по достижении которых, я задействую все имеющиеся ресурсы, чтобы новая статья предстала перед вами как можно быстрее.

Однако правила habr регламентируют применение механизмов, подобных голосованию за продолжение статей. И, возможно, благодаря именно этому нам так интересно читать публикуемый здесь материал.

Эта статья не станет исключением из этих правил. При этом любой из вас, кто захочет ускорить выход статьи о технологии "server", найдет способ выразить свое желание изучив README.md любого из репозиториев либо кликнув по ссылке в описании репозиториев.

В "noserver", один экземпляр будет обрабатывать одну очередь (один этап процесса). Такой подход может только разозлить духов я использую при отладке воркеров.

В зависимости от необходимой скорости обработки вы можете запустить сколь угодно много экземпляров «вручную».

Выглядеть это может так:

// app/src/Process/noserver/singleProcess.php

// Однопоточный обработчик заданий, приходящих в очередь

// Подключение конфигураций
include __DIR__ . '/../../../settings.php';
use App\ESB\pipeNcacheService;
use App\arrayNstring\queueDSN;
use App\arrayNstring\timeSpent;
use App\arrayNstring\progressString;

// Путь к воркерам
$path = __DIR__ . '/../worker';


// Настройка умолчаний
$queues = array_keys(WORKERS);
$queue = $argv[1] ?? end($queues);
$queue = strtolower($queue);
if (!in_array($queue, $queues))
    die("php $argv[0] <queue_name>" . PHP_EOL);

// Текстовой прогресс-бар
$progress = new progressString("Listenning... Idle: ", 40, 20);

// Секундомер, удобный для вывода на экран
$stopwatch = timeSpent::start();

// Конфигурирование и подключение beanstalkd-клиента
list($worker, $task) = WORKERS[$queue];
$procid = ['procid' => posix_getpid()];

// Работа с beanstalkd и Memcached,
// комбинированная в один класс (для удобства)
$dsn = new queueDSN($task, $queue, ...QUEUE_SERVER);

// Работа с адресами внутри ESB-шины
$pnc = new pipeNcacheService($dsn);
$pipe = $pnc->getPipe();

echo "Start listener for queue: $queue." . PHP_EOL;
echo "Press Ctrl-C to stop listener." . PHP_EOL;

// Прослушивание трубы в бесконечном цикле
// и запуск обработчика при получении задачи
while (true) {
    try {
        $job = $pipe->watch($queue)
            ->reserve(1);

        $now = new DateTime();
        $opts = json_encode($pipe->getPayload($job) + $procid);
        $pipe->delete($job);
        echo PHP_EOL . "Task recived at: " . $now->format('H:i:s') . 
                         " Starting worker: $worker. ";

        $stopwatch = timeSpent::start();
        exec("php $path/$worker $opts", $out);
        echo "Finished. Time spent: $stopwatch" . PHP_EOL;
        $stopwatch = timeSpent::start();

    } catch (Throwable $exception) {
        echo $progress($stopwatch('%I:%S', null, $now));
    }
}

Бросается в глаза странный запуск воркера… Несмотря на то, что каждый из воркеров является PHP-объектом, я использовал exec(...).

Это сделано в целях экономии времени, чтобы не создавать отдельные воркеры для "noserver" либо не изменять воркер под цели запуска в режиме "server".

Пару слов о конфигурации и развертывании


Константы конфигурирования


За конфигурацию вашего экземпляра отвечает файл app/settings.php. Он должен быть создан Вами сразу после клонирования репозитория. Для этого нужно переименовать файл app/settings.php.dist. Все константы описаны внутри.

app/settings.php, кроме прочего, подключает файлы с другими константами.
app/queues.php содержит названия очередей и заданий
app/plugs.php содержит описание Chrome-плагинов
app/techs.php содержит вычисляемые константы

Утилиты


Для удобства обработки результатов работы процесса и размещения заданий есть несколько утилит. Утилиты запускаются из командной строки. Снабжены описаниями аргументов. Расположены: app/src/Utils.
    backup.php     - сохраняет очереди в файл
    clear.php      - чистит очереди
    exporter.php   - экспортирует из файла с сохраненной очередью 
                     пары адрес:порт
    givethejob.php - размещает задания процессу 
                     (источник - файл со списком адрес:порт).
                     может исключить часть адресов из списка
    restore.php    - восстанавливает сохраненную очередь

Тонкая настройка воркеров


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

// app/src/Process/worker/curlChecker.php

....
$worker = new curlChecker(
    [
        // Формальное имя воркера
        curlChecker::WORKER => 'curlchecker',

        // Сервер beanstalkd
        curlChecker::PIPE_HOSTPORT => implode(':', QUEUE_SERVER),

        // Сервер Memcached
        curlChecker::CACHE_HOSTPORT => implode(':', MEMCACHED),

        // Функция, инициализирующая доступ к БД. 
        // В данном случае - не применяется
        curlChecker::DB_SCRIPT => __DIR__ . '/../../../confdb.php',

        // Очередь, куда попадает сообщение при старте воркера
        // (может быть удобна при трассировке либо при создании веб-панели)
        curlChecker::INFO_START => CURL_START,

        // Очередь, куда попадает сообщение при успешном завершении воркера
        // (может быть удобна при трассировке либо при создании веб-панели)
        curlChecker::INFO_END => CURL_END,

        // Дополнительные поля, которые добавляются в сообщения
        // для трассировочных очередей
        // Есть аналогичная константа и для стартовой очереди
        curlChecker::INFO_ADDS_END => ['host', 'port']
    ],
    ['setupworker', 'config', 'logic']
);
....


Развертывание


Инструкция предполагает, что у вас в распоряжении 2 машины с установленной Ubuntu 18.04.
Одна для инфраструктуры (docker), другая для управления процессом (process).

docker

# Все действия предполагают root-привелегии.
# Предполагается, что не заняты следующие TCP-порты:
#     11300, 11211, 4444, 5930, 8080, 8081, 8082, 8083

# Установка необходимых пакетов.

sudo -s
apt -y update
apt -y install git snap
snap install docker
apt -y install docker-compose

# Cклонировать репозиторий с инфраструктурой в домашний каталог
# "не root-пользователя" и запустить контейнеры

cd ~
git clone \
   https://oauth2:YRGzV8Ktx2ztoZg_oZZL@git.ituse.ru/deploy/esb-infrastructure.git

cd esb-infrastructure
docker-compose up --build -d

# Вам потребуется чашка кофе и 3 минуты терпения после окончания
# процесса старта контейнеров.
# Примерно столько времени необходимо на развертывания web-панелей.

process

# Все действия предполагают привелегии обыкновенного пользователя.

# Установка необходимых пакетов.

sudo apt -y update
sudo apt -y install git php-common php-cli php-curl php-zip php-memcached composer

# Cклонировать репозиторий с проектом на process-машину в любой каталог,
# доступный для записи и до-установить необходимые php-пакеты

cd /var/www
git clone \
    https://oauth2:YRGzV8Ktx2ztoZg_oZZL@git.ituse.ru/deploy/clicker-noserver.git

cd clicker-noserver
composer update

# Сконфигурировать приложение. Для это в файле заменить строки "XXXXXXXX"

mv app/settings.php.dist app/settings.php

# Запустить обработчики очередей.

gnome-terminal \
  --tab -e 'bash -c "php app/src/Process/noserver/singleProcess.php curl"' \
  --tab -e 'bash -c "php app/src/Process/noserver/singleProcess.php timezone"' \
  --tab -e 'bash -c "php app/src/Process/noserver/singleProcess.php whoer"'

# Дать задание. Предполагается, что файл со списком прокси,
# формата адрес:порт - log/list.proxy

php app/src/Utils/givethejob.php ./log/list.proxy

Ждать, наблюдая за происходящим через web-панель (http://ip-адрес-докер-машины:8080).
Результат будет доступен в очереди located.

И в заключении


Удивительно, но написание и редактирование этой статьи потребовало больше времени, чем написание самого кода.

На мой взгляд все могло быть наоборот (и отличие во времени могло быть в несколько раз большим), если бы не две идеологии: Message Queue и Enterprise Service Bus.

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

Спасибо.
Only registered users can participate in poll. Log in, please.
Роботы. Зло или повседневность?
23.81% Зло. Владельцев и их приспешников нужно перевоспитывать.5
33.33% Повседневность. Оставить всех как есть.7
42.86% Роботы нужны. Присоединяюсь к борьбе с поисковиками за «бесплатный трафик».9
21 users voted. 13 users abstained.
Tags:
Hubs:
+12
Comments41

Articles

Change theme settings