Как стать автором
Обновить

Пишем HTTP proxy сервер с плагинами

Время на прочтение 7 мин
Количество просмотров 18K
imageОдно время всплыла тема, что в одной из онлайн-игр появилась такая довольно досадная штука, как каптча. Само по себе, отвлекаться от игры для ввода каптчи может обернуться не слишком хорошими последствиями, особенно, если вводишь её не с первого раза, могут и враги насолить. Но не в этом соль. Особенно плоха вещь для тех, кто использует локальных ботов. Те то, маленькие, спотыкаются об каптчу, и за это игра мнгновенно их штрафует потерей юнитов и ресурсов. Неприятная штука, что говорить.

Итак, задача:
Хочется, чтобы каптчу вводить не приходилось. Хоть если играешь сам, хоть если играет за тебя бот, если ты спишь.
Дополнительное условие: 40 часов времени (ибо паника на корабле).
Желательное условие: установочный файл под Windows.
Ещё одно желательное условие: результат должен занимать не более мегабайта.


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

Итак, что же делать?

Попытка 1

Написать системную тулзу, которая бы перехватывала HTTP запросы и ответы от всёх установленных программ, и фильтровала бы те ответы, на которые бы требовалось ввести каптчу, вводя её самостоятельно. Над программой, которая в том числе должна была решать и эту задачу корпели примерно полгода два белорусских программиста, меняя платформу с C на C#, и потом на Java, и мирясь с тем, что им может понадобится установленный на машину OpenSSL. Задача каждый раз обрастала ненужными подробностями. Ну, в целом, не вышло.

Попытка 2: Сам, всё сам


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

Выбор платформы

Выбор был не сложен, особенно учитывая попытку 1. C и C# были быстро отметены, учитывая полное отсутствие опыта. Были выявлены следующие, богатые необходимыми библиотеками, платформы:

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

Python Как известно, работает везде и всё включено (batteries included). Весит 7Мб. По современным меркам, конечно, немного, но всё же хотелось компактнее. Остался вопрос, как этот инсталлятор внедрить в инсталлятор моей утилиты. Не знаю, как это делается у питоновских приложений, возможно, намного проще, но инсталлятор в инсталляторе я как-то раз уже делал и больше не хочу.

Ruby На момент начала моих поисков отсутствовал одношаговый инсталлятор под винду. Целиком и полностью. Сейчас есть, подразумевает установку MinGW, MSYS и прочего, при установке могущего испугать юзера. Вес 7Мб.
Про инсталлятор в инсталляторе вопрос остался.

Lua Очень давний и популярный среди скриптовщиков С++ игр язык. Вялое community, разрозненные библиотеки. Вес кастомной сборки VM в нужными библиотеками — всего 800Кб. Инсталлятор не предоставляется, есть набор exe файлов, которым в качестве параметра передаётся lua скрипт для его запуска. То, что нужно, так ещё и откомпилено под Win, MacOS, Linux, каждое из них в версиях 32 и 64 отдельно. То, что надо.

Итак, я взялся за изучение Lua (сбылось новогоднее пожелание, я изучил новый язык программирования).
Язык обладает чудесными свойствами, такими как:
— sandboxing (в ruby был только патч для версии 1.8.5): позволяет запускать сторонний код, ограничивая ему окружение;
— coroutines (типа ruby fibers из 1.9): позволяет сделать очень легковесную кооперативную многозадачность;
— очень простые (точнее, простая — есть только ассоциативный массив) структура данных, которой, как оказалось, хватает, для выполнения большинства задач по обработке данных;
… ещё много всего, тяжко так в одном посте.

Проще всего было сделать такую систему в виде фильтрующего запросы и ответы HTTP прокси сервера, что и было решено сделать (от добра добра не добра).

Идея проста: повесить сервер TCP, слушать, что клиент просит, разбирать заголовки HTTP, искать HOST, убирать HTTP header «Proxy-Connection», отсылать запрос тому, кому он предназначался, получать ответ, направлять клиенту и т.п.

Ответ сервера нужно фильтровать, и делать это можно, если сервер не применяет HTTPS, а он по счастью его не применяет. Сделать это оказалось довольно просто, достаточным оказалось написать сокращённый до 190 строк Lua аналог mechanize для Ruby, который делает с заголовками и телом запроса что ни взбредёт в голову, позволяя писать какие хочется фильтры для HTTP запросов.

Ну, в этом случае нам надо было избавиться от вредной reCAPTCHA, для чего всего лишь потребовалось определить:
— к странице ли травиана щёл исходный запрос (и запрашивалась ли страница HTML):
string.find(request.uri(), 'travian') and mimetype and string.find(mimetype, 'text/html')

— содержалась ли на результирующей странице каптча вместо «полезных» игровых данных:
local captcha, captcha_key = string.match(response.body(), '<iframe src="(http://api.recaptcha.net/noscript??(k=[%a%d_]+&lang=en))')


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

В итоге, скачивалась картинка каптчи, отправлялась индусам (2 раза на всякий случай), полученные через 5-10 секунд ответы сравнивались, и если они были одинаковыми, то результат отправлялся травиану HTTP POST запросом, на что жертва выдаёт страницу, радостно сообщая, что мы, оказывается, человек, и кучу «полезных» игровых данных. Эту страницу мы и показываем ничего не подозревающему клиенту, который мог заметить лишь некоторую паузу. В пессимистичном случае несовпадения картинка отправлялась индусам ещё раз, и так далее до того момента, пока не получалось хотя бы два одинаковых решения, остальные отправлялись в службу поддержки на возврат денег (практически никакой халявы для индусов).

Итак, вот оно, решение и есть.

Однако, случилось то, о чём никто не мог предполагать. Пользователям зачем-то понадобился ещё и доступ к другим сайтам, немыслимо! Среди них оказались даже сайты, доступ к которым осуществлялся через HTTPS, и с этим нужно было что-то делать без регулярных переключений прокси включён-выключен.
И ещё нужно было, чтобы несколько запросов принимались одновременно. Неприятным оказалось то, что google analytics иногда делает запрос, длящийся минуты, оставляя однопоточную прокси в режиме ожидания.

Ну что же, для этого нашлось целых три разных библиотеки для создания асинхронного TCP сервера. То есть — ждём входящее соединение, получаем кусок данных, передаём управление диспетчеру, смотрим есть ли ещё входящие соединения или открытые соединения для которых есть данные (select/kpoll/epoll), передаём управление по очереди.

Увы и ах, поскольку все такого рода соединения происходят на локальной машине, то всё это происходит почти мнгновенно. А медленные соединения — исходящие. Подковырять существующие библиотеки (copas, asok), которые предназначены для мультиплексирования входящих соединений, оказалось сложнее, чем писать свою. И я написал небольшую (272 строки) свою. Помимо того, что все входящие и исходящие соединения работают асинхронно, так можно добавлять ещё любые корутины (поправьте меня, люди с профильным образованием) в пул, работающий в общем цикле.

Ну вот, всё стало работать параллельно, а по скорости лишь незаметно отставать от того, как это работает без прокси.

Как же велико было моё удивление, когда я получил от сервера страницу с (в том числе) заголовками:
Content-Encoding: gzip
Transfer-Encoding: chunked
и собственно полные кракозябры в качестве тела ответа.

Первая мысль была отключить в запросе Accept-Encoding, чтобы сервер не пытался паковать данные, и переделать HTTP 1.1 в HTTP 1.0, чтобы не посылало «чанками». Но подумал об падении скорости и увеличении трафика, и сжалился над пользователями.
Вышло так:
if headers(pipe, target)['Transfer-Encoding'] == 'chunked' then
target.body = dechunk(target.body)
end

function dechunk(chunkie)
local chunk_size
local chunk
local chunks = {}
chunkie, chunk_size = readline(chunkie)

while chunk_size and tonumber(chunk_size, 16) > 0 do
chunkie, chunk = readbytes(chunkie, tonumber(chunk_size, 16))

table.insert(chunks, chunk)
chunkie, chunk_size = readline(chunkie)
if not chunk_size or chunk_size == '' then -- sometimes there's a crlf, sometimes not
chunkie, chunk_size = readline(chunkie)
end
end

return table.concat(chunks)
end


Ушёл читать матчасть. Слава богу, по этим пунктам документации порядочно.
Склеиваем «чанки», получаем gzip файл (иногда deflate, но мне не встречался пока). Распаковываем (спасибо David Manura за библиотеку).
Распаковка вышла ещё проще:
if headers(pipe, target)['Content-Encoding'] == 'gzip' and #target.body > 0 then
local decoded = {}
gzip.gunzip {input=target.body, output=function(byte) table.insert(decoded, string.char(byte)) end}
target.body = table.concat(decoded)
end


Осталось немного
— сделать HTTPS-туннелирование для HTTPS сайтов (слава богу, OpenSSL бандлить не надо, просто данные прозрачно передавать туда-сюда):
if request.method() == 'CONNECT' then
local sent_to_server, err = client.send("HTTP/1.0 200 Connection established\r\nProxy-agent: BotHQ-Agent/1.2\r\n\r\n")
print('https transparent connection')
https(client, server)
return
end

local function https(client, server)
close_callback = function()
client.close()
server.close()
end

client.receive_subscribe(function(data)
server.send(data)
end, close_callback)

server.receive_subscribe(function(data)
client.send(data)
end, close_callback)
end


— положить в инсталлятор:
В целом приключения с запуском 7zip sfx на Heroku стоят отдельного поста. Радость победы затмевает любые сложные моменты разработки.

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

В итоге:
Сам прокси сервер на 71 строку здесь.
Асинхронная библиотека TCP-сервер-клиент на 272 строки тут.
Некий аналог HTTP клиента на 190 строк.
Фильтр для разгадывания каптчи на 150 строк тут.
Установочный файл размером менее мегабайта.

Уверен, такой штуке найдётся много полезных применений, начиная от не очень хороших, типа спамилок с «автоматической» разгадкой каптчи, до полезных, когда нужно гибко отфильтровать пользовательский трафик скриптом. Приведу простейший скрипт, не позволяющий пользователям, подсоединённым через прокси, соединяться с vk.com:
module(..., package.seeall)
function filter(request, response)
response.set_body('')
end

function pre(request, response)
return string.find(request.uri(), 'vk.com')
end


С приближающимся выходом lua 5.2 снимется ограничение на вызов метаметодов из корутин, и библиотеки можно будет сделать более красивыми, например, уйдут методы http.set_body, и многое другое.
Теги:
Хабы:
+19
Комментарии 36
Комментарии Комментарии 36

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн