klink0v (klink0v) wrote,
klink0v
klink0v

Пишем свой собственный веб-сервер на bash

Имеется в виду не "башорг", а "линуксовый шелл". И да, сперва я хотел озаглавить этот псто "Я потный извращенец", но потом всё-таки передумал.

Задача. Программисты из соседнего отдела попросили предоставить им какой-нибудь API для доступа к корпоративному Asterisk-серверу. Хотят иметь возможность из своих приложений закачивать на телефонную станцию WAV-файлики, набирать чей-нибудь номер, проигрывать поциенту WAV-файл, а потом смотреть что из этого получилось или не-получилось.

Настройка Asterisk-а — это целая отдельная песня, тут я касаться данной темы не буду. Упомяну лишь, что для решения поставленной задачи необходимо обязательно каким-то образом поиметь вышеобозначенный WAV-файл на локальном жёстком диске, в противном случае никаких других возможностей воспроизвести его средствами самого Asterisk-а нет. К тому же, его перед этим очень неплохо бы ещё проверить на валидность, перекодировать в A-Law (в моей телефонной сети везде используется именно этот кодек) и вернуть программистам ID (идентификатор) созданной задачи и длительность загруженной записи в секундах (чтобы они могли потом проанализировать результат своих действий). Долго думал, какую шашку взять в руки для реализации всей этой радости. Из PHP неудобно делать системные вызовы, с Perl-ом я не дружу. Остановился на bash-е (встроенный в большинство дистрибутивов линукс системный шелл).

Потом подумал — из-под чего запускать свои скрипты? Ставить апач на сервак мучительно не хотелось. Да и зачем мне вообще нужен там веб-сервер? Ведь для решения таких простых задач можно написать его самостоятельно. Целиком выкладывать здесь я свой скрипт не буду, но некоторые самые интересные моменты постараюсь изложить.

Начнём с того, что кто-то должен открыть сокет и слушать HTTP-порт. На чистом баше это сделать затруднительно, поэтому привлекаем xinet.d примерно вот с такой конфигурацией:

Изначально xinet был придуман для экономии системных ресурсов, чтобы какой-нибудь FTP-сервер не "висел" бы постоянно резидентом и не кушал бы почём зря оперативную память, особенно если им пользуются полторы калеки в неделю. Вместо него сокет слушает компактный xinetd, который по мере обращений клиентов запускает приложение и передаёт ему данные через stdin/stdout. Но в качестве запускаемого приложения может быть что угодно, в том числе и шелл-скрипт. Чем мы и воспользуемся.

Не забываем про sha-bang, определяем глобальные параметры.

Дальше напишем функцию отлупа с ошибкой, поскольку нам придётся ссылаться на этот кусок кода очень часто. Тут есть два нюанса. Во-первых, не забываем полностью отдавать все HTTP-заголовки. Во-вторых, нужно убедиться, что мы забрали у клиента все данные, которые он хотел нам передать. Последнее представляет собой небольшую сложность. Дело в том, что EOF (End-of-file) в stdin (стандартный ввод) наш скрипт получит только в момент закрытия соединения. Только вот многие HTTP-клиенты исповедуют Keep-Alive и соединение не закрывают. Остаётся вариант парсить заголовок "Conent-Length" и забирать из потока стандартного ввода (stdin) ровно столько байт, сколько там указано. Но такой подход не защитит наш скрипт от зависания в случае некорректно сформированного HTTP-запроса, когда в Content-Length заявлено больше, чем клиент собирался передать на самом деле. Поэтому я поступаю проще: жду ровно одну секунду и говорю "Адьос, амиго! Кто не успел, тот опоздал".

Следующая функция. Забираем из запроса клиента заголовки. По большому счёту нас интересует только первая строка, содержащая метод запроса (POST или GET) и URL запроса. В ней метод будет первым словом до пробела, REQUEST — вторым словом. Третье слово отражает версию протокола HTTP, которая нам не особо нужна.

Функция закачки (upload) звукового файла на сервер. В bash-е это "some kind of black magic". В скрипте ниже предполагается, что переменная "$PARM" содержит гарантированно корректный номер вызываемого абонента либо ничего (смотри последний в этом посте листинг), "$ACTION" — подстрока в URL-е между символами "/" и "?" (аналог REQUEST_URI). ID — некий уникальный идентификатор а-ля "автоинкремент", который мы каждый раз сохраняем в файл и читаем при новом очередном запуске скрипта. Самое интересное начинается со строки "read boundary". По задумке, к этому моменту функция "catch_request" (см. выше) уже отработала, заголовки запроса уже получены и благополучно проигнорированы, дальше идет delimiter аттача и сам аттач.

Тут возникает та же самая проблема, что уже упоминалась выше. Непонятно сколько байт нужно забрать из сокета (читай, "стандартного ввода"). "Правильный" способ — отпарсить Content-Length, вычесть из неё длину самих разграничителей, длины заголовков и учесть, что некоторое количество байт соответствует символам возврата каретки ("\r"). После этого прочитать из stdin полученное количество байт либо при помощи dd, либо "head -c". Но мне такой вариант по некоторым причинам не понравился, поэтому я просто "втупую" беру tee (разветвление) и сохраняю stdin в файл, попутно проводя поиск закрывающего разграничителя. И как только нахожу, принудительно "грохаю" процесс "tee" отсылкой ему SIGTERM. Единственное, без промежуточного файла тут не обойтись, потому как сам закрывающий разграничитель сам по себе тоже попадает под "условие отбора". Построчно же читать-анализировать-дописывать в данном случае нельзя, ибо размер переменной в шелле имеет ограничение по размеру в 255 байт, и не всякая птица долетит прочитанная строка из octet-stream туда поместится. Поэтому приходится сначала сохранять, а потом делать "head -n -1 | head -c -2", то есть удалить последнюю строку и последние 2 байта. Наверное, красивее было бы sed-ом, но так тоже вполне работоспособно.

После того, как закачали аудиосэмпл, пытаемся сконвертировать его sox-ом в A-LAW и смотрим что получилось. Если всё хорошо — определяем длительность записи в секундах и возвращаем её клиенту вместе с другой сопроводительной информацией.

В моём скрипте присутствуют ещё несколько функций, как то: чтение текущего ID задачи из файла, вывод тестовой странички с отладочной информацией, вывод приглашения (промпта) для клиента-человека (в целях тестирования), запрос у Asterisk-а количества занятых/доступных каналов в транке, запуск задачи на выполнение, удаление успешно отработанной задачи. Здесь это всё я выкладывать не буду. Во-первых, получится много; во-вторых жадный я. Напоследок покажу только, как я запускаю эти функции. Самое интересное и забавное здесь то, что Bash начиная с какой-то версии внезапно тоже умеет работать с регулярными выражениями. Я этим пользуюсь для того, чтобы выделить из URL-а десять цифр, которые потом играют роль параметра ("$PARM") в приведённых выше функциях. Как видно из кода, найденное в строке соответствие шаблону попадает в предопределённый массив под названием "BASH_REMATCH[]".

Вот так мы победили сырость сделали простенький веб-сервер на баше.

Когда я показал это своему коллеге - виндовому админу, он полушутя изрёк: "Да-а-а-а-а, Стас. У тебя [писька] больше. Я такое на Powershell-е написать не могу".

Tags: bash, linux, scripting, администрирование
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 1 comment