[Из песочницы] На что способен мозг студента, познающего компьютерный мир
Доброго времени суток.
Закончив писать очередной скрипт на Bash, понял, что всё должно быть совершенно иначе, однако всё работало. Хочу вам показать, какие непотребства и костыли написал я, дабы решить задачу, но пока не имея вагона знаний. Иначе говоря, каррикатура на программирование.
Стало нужно что-то, что бы:
- Выводило множество рифм для слова, за исключением квадратов
- Пересекало множества рифм двух слов
Для чего? Ну вот надо — и всё тут.
Кто не знает, квадратная рифма (в просторечии — квадрат) — два слова, у которых совпадают две последние буквы в написании, что (зачастую, только это) и делает их рифмой. Например, розы — морозы; шина — машина. Использование квадратов в современном стихосложении не особо одобряется людьми, ввиду их примитивности.
Самым простым решением мне показалось написать скрипт на Bash, использующий уже существующий генератор рифм — HOST, который в первую очередь подбирает их по созвучиям, а не по написанию. Что за HOST? Потому что если указать настоящее название сайта — скажут, что реклама. Почему бы не продолжить пользоваться им? Во-первых, несмотря на его преимущество подбора рифм по созвучиям, он-таки частенько выдаёт квадраты. Во-вторых, всё равно приходится думать мозгами, тратить время на переключение между вкладками, силы на запоминание повторяющихся слов в списках для нахождения рифмы к двум словам.
Получение сильных рифм
Что я знаю? Я знаю про утилиту wget, которая скачивает страницу по указанному URL. Хорошо, выполняем запрос — получаем HTML страницу в файле, который назван словом для рифмы. К примеру, поищем по слову «здесь»:
wget https://HOST/rifma/здесь
Но мне же нужен только список слов, как избавится от всего остального? Смотрим и видим, что список слов оформлен, как бы это ни было странно, в виде списка, и слова находятся в тегах <li></li>. Чтож, у нас есть прекрасная утилита sed — так и запишем:
cat $word | grep '' | sed -e "s% %%" | sed -e "s% %%" | sed -e "s/ //g" | sed -e "/^$/d" 1> $word
Сначала из файла word выбираем строки, в которых содержится тег <li> — получаем кучу пустых тегов и строки со словами. Убираем сам тег и его закрывающий — здесь символы процента использованы вместо слешей потому, что в самом теге </li> уже есть слеш, отчего sed немного вас не понимает. А с процентами всё хорошо. Убираем все пробелы из файла, удаляем пустые строки. Вуаля — готовый список слов.
Для того, чтобы убрать слова, рифмующиеся за счёт последних букв, выделим последние две буквы из исходного слова и почистим список:
squad=${word:((${#word}-2)):2}
cat $word | sed -e "/.$squad$/d" 1> $word
Смотрим, пробуем — всё работает… так, а где список для слова «играть»? А для слова «иду»? Файл пустой! А это всё потому, что эти слова — глаголы, и мы знаем, что делают с теми, кто рифмует на глаголы. Глагольная рифма хуже даже квадратной, ибо глаголов в русском языке больше всего, да ещё и все с одинаковыми окончаниями, из-за чего их и не оказалось в итоговом файле после проверки окончаний.
Однако не спешим. К каждому слову есть не только рифмы, но ещё и ассонансы, которые иногда звучат куда лучше, чем рифма — на то они и ассонансы (фр. assonance, от лат. assono — звучу в лад).
Получаем ассонансы
Здесь начинается самое интересное: ассонансы появляются по отдельному URL, а на этой же странице, путём выполнения скрипта, посылания HTTP запроса и получения ответа. Как же сказать wget'у нажать кнопочку? А вот никак. Печально.
Заметив, что URL в строке всё же как-то меняется, я скопировал то, что было там после перехода на ассонансы, и вставил в новой вкладке браузера — открылись сильные рифмы. Не то.
По сути, подумал я, для сервера должно быть всё равно, выполняется ли скрипт, отправляющий ему запрос, или же человек сам руками набирает его. Так? А кто его знает, пойдём проверять.
Куда отправлять? Что отправлять? HTTP запрос на IP сервера, там что-то вроде GET… там потом что-то HTTP/1.1… Надо посмотреть, что и куда отправляет браузер. Устанавливаем wireshark, смотрим трафик:
0040 37 5d a3 84 27 e7 fb 13 6d 93 ed cd 56 04 9d 82 7]£.'çû.m.íÍV...
0050 32 7c fb 67 46 71 dd 36 4d 42 3d f3 62 1b e0 ad 2|ûgFqÝ6MB=ób.à.
0060 ef 87 be 05 6a f9 e1 01 41 fc 25 5b c0 77 d3 94 ï.¾.jùá.Aü%[ÀwÓ.
Эм… что? Ах да, у нас же HTTPS. Что делать? Устроить MITM атаку на себя? Идеально, жертва сама будет нам помогать.
В общем, догадавшись полазить по браузеру, я-таки нашёл сам запрос, и адресата. Поехали:
telnet IP PORT
Trying IP...
Connected to IP.
Escape character is '^]'.
GET /rifma/%D0%BC%D0%B0%D1%82%D1%8C?mode=block&type=asn HTTP/1.1
Host: HOST
Accept-Language: en-US,en;q=0.5
X-Requested-With: XMLHttpRequest
Connection: close
HTTP/1.1 400 Bad Request
Server: nginx/1.8.0
Date: Sun, 03 Nov 2019 20:06:59 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 270
Connection: close
400 The plain HTTP request was sent to HTTPS port
400 Bad Request
The plain HTTP request was sent to HTTPS port
nginx/1.8.0
Connection closed by foreign host.
Хы. Хы-хы. Действительно, что я ожидал, отправляя голый HTTP запрос на HTTPS порт. Разве шифровать теперь? Вся эта возня с RSA ключами, потом еще с SHA256. А зачем, есть же OpenSSL для таких дел. Чтож, уже знаем, что делать, только предварительно уберём поля Referer и Cookie — думаю, они не сильно повлияют на дело:
openssl s_client -connect IP:PORT
{Всякие ключи, сертификаты}
GET /rifma/%D0%B7%D0%B4%D0%B5%D1%81%D1%8C?mode=block&type=asn HTTP/1.1
Host: HOST
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/javascript,text/html,application/xml,text/xml,*/*
Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Connection: keep-alive
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Status: 200 OK
Date: Sun, 03 Nov 2019 20:34:33 GMT
Set-Cookie: COOKIE
X-Powered-By: Phusion Passenger 5.0.16
Server: nginx/1.8.0 + Phusion Passenger 5.0.16
Expires: Thu, 01 Jan 1970 00:00:01 GMT
Cache-Control: no-cache
Strict-Transport-Security: max-age=31536000
Content-Security-Policy: block-all-mixed-content
Content-Encoding: gzip
Это что, мат на серверном? Хорошо, по крайней мере мне ответили 200 OK, значит, куки и реферер ни на что не влияют. Сжатие gzip, но при копировании копируются ASCII символы. Точно, можно убрать строку Accept-encoding. Всё прекрасно — получаем HTML документ, теперь уже с ассонансами. Но вот два вопроса: как запускать OpenSSL и передавать ему данные скриптом? И как считывать вывод, если после получения ответа мы остаемся как бы в «оболочке» OpenSSL? Если со вторым можно что-то придумать, но вот с первым…
Как хорошо, что есть Хабр, где я прочитал про утилиту expect, которая автоматизирует процесс взаимодействия с программами, ожидающими взаимодействия с человеком. Ещё более привлекательно наличие команды autoexpect, генерирующей expect скрипт по вашим действиям. Чтож, запускаем, делаем всё это и вот готовый скрипт. Только уж очень он огромный, а всё потому, что OpenSSL выводит сертификаты, ключи, а expect ожидает вывода этого всего. Надо ли нам это? Нет. Сносим всё первое приглашение, оставляя только последний перенос строки '\r'. Также из нашего запроса убираем поля User-Agent и Accept — ни на что не влияют. Так, запускаем. Скрипт выполнился, но где заветный HTML документ? Expect съел его. Дабы заставить его выплюнуть, нужно положить:
set results $expect_out(buffer)
перед концом скрипта — так будет записан вывод исполняемой expect'ом команды и выведен на экран. По итогу, что-то вроде этого:
#!/usr/bin/expect -f
set timeout -1
spawn openssl s_client -connect IP:PORT
match_max 100000
expect -exact "
---\r
"
send -- "GET /rifma/%d0%b7%d0%b4%d0%b5%d1%81%d1%8c?mode=block&type=asn HTTP/1.1\rHost: HOST\rAccept-Language: en-US,en;q=0.5\rX-Requested-With: XMLHttpRequest\rConnection: close"
expect -exact "GET /rifma/%d0%b7%d0%b4%d0%b5%d1%81%d1%8c?mode=block&type=asn HTTP/1.1\r
Host: HOST\r
Accept-Language: en-US,en;q=0.5\r
X-Requested-With: XMLHttpRequest\r
Connection: close"
send -- "\r"
set results $expect_out(buffer)
expect -exact "\r
"
send -- "\r"
expect eof
Но и это ещё не всё! Как можно было заметить, во всех примерах URL запроса был статичным, однако именно он отвечает за то, к какому слову будут выведены ассонансы. А так получается, что мы постоянно будем искать по слову »%d0%b7%d0%b4%d0%b5%d1%81%d1%8c» в ASCII или «здесь» в UTF-8. Что делать? Конечно же просто напросто каждый раз генерировать новый скрипт, друзья! Только уже не autoexpect'ом, а с помощью echo, т.к. у нас в новом не меняется ничего, кроме слова. И да здравствует новая проблема: как бы нам этак по-умному перевести слово с кириллицы в URL формат? Что-то и для терминала-то особо нет ничего. Ну ничего, мы же можем? Можем:
function furl {
furl=$(echo "$word" | sed 's:А:%d0%90:g;s:Б:%d0%91:g;s:В:%d0%92:g;s:Г:%d0%93:g;s:Д:%d0%94:g;s:Е:%d0%95:g;s:Ж:%d0%96:g;s:З:%d0%97:g;s:И:%d0%98:g;s:Й:%d0%99:g;s:К:%d0%9a:g;s:Л:%d0%9b:g;s:М:%d0%9c:g;s:Н:%d0%9d:g;s:О:%d0%9e:g;s:П:%d0%9f:g;s:Р:%d0%a0:g;s:С:%d0%a1:g;s:Т:%d0%a2:g;s:У:%d0%a3:g;s:Ф:%d0%a4:g;s:Х:%d0%a5:g;s:Ц:%d0%a6:g;s:Ч:%d0%a7:g;s:Ш:%d0%a8:g;s:Щ:%d0%a9:g;s:Ъ:%d0%aa:g;s:Ы:%d0%ab:g;s:Ь:%d0%ac:g;s:Э:%d0%ad:g;s:Ю:%d0%ae:g;s:Я:%d0%af:g;s:а:%d0%b0:g;s:б:%d0%b1:g;s:в:%d0%b2:g;s:г:%d0%b3:g;s:д:%d0%b4:g;s:е:%d0%b5:g;s:ж:%d0%b6:g;s:з:%d0%b7:g;s:и:%d0%b8:g;s:й:%d0%b9:g;s:к:%d0%ba:g;s:л:%d0%bb:g;s:м:%d0%bc:g;s:н:%d0%bd:g;s:о:%d0%be:g;s:п:%d0%bf:g;s:р:%d1%80:g;s:с:%d1%81:g;s:т:%d1%82:g;s:у:%d1%83:g;s:ф:%d1%84:g;s:х:%d1%85:g;s:ц:%d1%86:g;s:ч:%d1%87:g;s:ш:%d1%88:g;s:щ:%d1%89:g;s:ъ:%d1%8a:g;s:ы:%d1%8b:g;s:ь:%d1%8c:g;s:э:%d1%8d:g;s:ю:%d1%8e:g;s:я:%d1%8f:g;s:ё:%d1%91:g;s:Ё:%d0%81:g')}
Итого имеем скрипт, преобразующий слово в ASCII текст, генерирующий уже другой скрипт, который запрашивает через OpenSSL у сервера страничку сайта с ассонансами. А дальше перенаправляем вывод последнего скрипта в файл и по-старинке пропускаем его через «фильтры» лишнего, квадратов и дозаписываем в файл.
Пересечение множеств. Итог
Собственно это именно то, что вызывает наименьшие проблемы. Выполняем вышеуказанные процедуры для двух слов, затем из двух списков сравниваем каждое слово с каждым и если совпадение найдено — выводим. Теперь у нас есть скрипт, который принимает на вход два слова и выводит список слов, рифмующихся и с тем и с другим, да ещё и с учётом ассонансов, и это все без ручного переключения между четырьмя вкладками и запоминанием слов «на глаз» — всё собрано, учтено и выброшено автоматически. Прекрасно.
Целью данной публикации было показать, что если человеку что-то нужно, то он это сделает в любом случае. Очень неэффективно, криво, жутко, но то будет работать.