CGI-программирование на ассемблере?!? – Легко! — Архив WASM.RU

Все статьи

CGI-программирование на ассемблере?!? – Легко! — Архив WASM.RU

Введение.

В этой статье я хочу рассказать о CGI интерфейсе вообще, его реализации для windows и использовании при написании CGI-программ языка ассемблер в частности. В рамки этой статьи не входит полное описание CGI, так-как в Интернете материала по этому вопросу просто море и пересказывать все это здесь я просто не вижу смысла.

Теория CGI.

CGI – (Common Gateway Interface) – Общий Шлюзовый Интерфейс. Как не трудно догадаться интерфейс этот служит шлюзом между сервером (здесь я подразумеваю программу - сервер) и какой-либо внешней программой написанной для ОС на которой этот самый сервер запущен. Таким образом CGI отвечает за то, каким именно образом данные будут переданы от программы-сервера к CGI-программе и обратно. Интерфейс не накладывает никаких ограничений на то, на чем должна быть написана CGI-программа, это может быть как обычный исполнимый файл, так и любой другой файл – главное, чтобы сервер смог его запустить (в среде windows это например может быть файл с расширением, привязанным к какой-либо программе).

С момента когда Вы вызвали (например нажали кнопку формы, к которой привязан вызов CGI-программы) CGI-программу до получения вами результата в окно браузера происходит следующее:

- Вэб-клиент (например браузер) создает подключение к серверу, указанному в URL;

- Вэб-клиент посылает запрос серверу, запрос этот обычно делается с помощью двух методов GET или POST;

- Данные из запроса клиента (например значения полей формы) передаются сервером, используя CGI-интерфейс, CGI-программе, указанной в URL;

- CGI-программа обрабатывает данные клиента, полученные от сервера и генерирует на основе этой обработки ответ клиенту, который она передает по все тому же CGI-интерфейсу серверу, а он в свою очередь передает его уже непосредственно клиенту;

- Сервер разрывает соединение с клиентом.

В стандартной спецификации CGI принято, что сервер может обмениваться с программой следующими способами:

- Переменные окружения – они могут быть установлены сервером при запуске программы;

- Стандартный поток ввода (STDIN) – с его помощью сервер может передать данные программе;

- Стандартный поток вывода (STDOUT) – программа может писать в него свой вывод, передающийся серверу;

- Командная строка – в ней сервер может передать некоторые параметры программе.

Стандартные потоки ввода/вывода весьма удобны и широко используются на UNIX-системах, чего не скажешь о windows, поэтому существует спецификация CGI, разработанная специально для windows-систем так и называемая «Windows CGI». Но, естественно, и стандартные потоки ввода/вывода так же можно использовать в windows CGI программировании. Здесь я не буду затрагивать стандарт «Windows CGI», и на это существует по крайней мере две причины – первая, и самая главная – на данный момент не все http-сервера под windows поддерживают эту спецификацию (в частности мой любимый Apache 1.3.19). Вторую причину вы можете наблюдать набрав в любой поисковой системе строчку «Windows CGI». Отмечу относительно этого интерфейса лишь общие детали – все данные от сервера к клиенту передаются посредством обычного для windows *.ini файла, имя которого передается программе в командной строке. При этом все данные в файле уже заботливо разбиты по секциям сервером и вам лишь остается используя функции «GetPrivateProfile*» извлечь их оттуда. Ответ серверу передается опять же посредством файла, имя которого указано в соответствующей записи ini-файла.

Какие же данные могут быть переданы клиентом CGI-программе? – практически любые. В общем случае программе передаются значения полей формы, которые заполняет клиент, но это также могут быть и какие-либо двоичные данные, например файл с картинкой или музыкой. Данные могут быть переданы на сервер двумя различными методами – это метод GET и метод POST. Когда мы создаем форму для заполнения на нашей страничке мы явно указываем каким из приведенных методов мы хотим отправить введенные пользователем данные, делается это в основном тэге формы примерно так:

<form name="form_1" method=get action="/cgi-bin/name_script">

При отправке данных методом GET данные браузером считываются из формы и помещаются следом за URL скрипта, за знаком вопроса, если значимых полей в форме несколько, то они передаются все через значёк «&», имя поля и его значение пишутся в URL через знак «=». Например запрос, сгенерированный браузером из формы при нажатии на кнопку, к которой привязан скрипт «/cgi-bin/test.exe», при учете что первое поле формы называется «your_name», второе – «your_age», может выглядеть так:

GET /cgi-bin/test.exe?your_name=Pupkin&your_age=90 HTTP/1.0

Использование метода GET имеет сразу несколько слабых сторон – первое и самое главное – т.к. данные передаются в URL то он имеет ограничение на количество этих самых передаваемых данных. Вторая слабость опять же вытекает из URL – это конфиденциальность, при такой передаче данные остаются абсолютно открытыми. Итак, хорошо если у нас в форме 2-3 небольших поля… встает вопрос что же делать если данных больше? Ответ – использовать метод POST!

При использовании метода POST данные передаются серверу как блок данных, а не в URL, что несколько развязывает нам руки для увеличения объема передаваемой информации, для вышеприведенного примера формы POST блок, посылаемый серверу будет примерно такой:

POST /cgi-bin/test.exe HTTP/1.0

Accept: text/plain

Accept: text/html

Accept: */*

Content-type: application/x-www-form-urlencoded

Content-length: 36

your_name=Pupkin&your_age=90

Как уже говорилось выше, после получения данных сервер должен преобразовать их и передать CGI программе. В стандартной спецификации CGI введенные клиентом данные при запросе GET помещаются сервером в переменную среды программы «QUERY_STRING». При запросе POST данные помещаются в стандартный поток ввода приложения, откуда могут быть им считаны. Кроме того, при таком запросе сервером устанавливаются еще две переменные среды - CONTENT_LENGTH и CONTENT_TYPE, по которым можно судить о длине запроса в байтах и о его содержании.

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

REQUEST_METHOD

Описывает каким именно методом получены данные

Пример:REQUEST_METHOD=GET

QUERY_STRING

Строка запроса, если использовался метод GET

Пример:QUERY_STRING= your_name=Pupkin&your_age=90&hobby=asm

CONTENT_LENGTH

Длина в байтах тела запроса

Пример:CONTENT_LENGTH=31

CONTENT_TYPE

Тип тела запроса

GATEWAY_INTERFACE

Версия протокола CGI

Пример:GATEWAY_INTERFACE=CGI/1.1

REMOTE_ADDR

IP-Адрес удаленного хоста, то бишь клиента, нажавшего кнопочку в форме

Пример:REMOTE_ADDR=10.21.23.10

REMOTE_HOST

Имя удаленного хоста, это может быть его доменное имя или например имя компьютера в среде Windows, если таковые получены быть не могут, то поле содержит его IP

Пример:REMOTE_HOST=wasm.ru

SCRIPT_NAME

Имя скрипта, использованное в запросе.

Пример:SCRIPT_NAME=/cgi-bin/gols.pl

SCRIPT_FILENAME

Имя файла скрипта на сервере.

Пример:SCRIPT_FILENAME=c:/page/cgi-bin/gols.pl

SERVER_SOFTWARE

Программное обеспечение сервера

Пример:Apache/1.3.19 (WIN32)

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

В общем-то это вкратце все, для получения более подробной информации об Общем Шлюзовом Интерфейсе смотрите специализированную документацию, это описание я сделал для того, чтобы напомнить вам, а если не знали то ввести в курс дела. Давайте попробуем что-нибудь сделать на практике.

 

Практическая часть.

Для практики нам понадобятся как минимум 3 вещи – какой-нибудь http-сервер для Windows, все примеры я пробовал на Apache 1.3.19 для Windows, сервер бесплатный, скачать его можно с http://httpd.apache.org/download.cgi. Да, и сервер нам понадобится не абы – какой, а настроенный для запуска cgi-скриптов! Как это делается для сервера используемого вами смотрите документацию. Вторая вещь, которая нам понадобится это, естественно, ассемблер, так же необходимо, чтобы компилятор поддерживал создание консольных WIN32 приложений, я использую Tasm, но прекрасно подойдут и Fasm и Masm и множество других *asm’ов. Ну и наконец самое главное, что потребуется это желание.

Итак, я допускаю, что сервер был вами благополучно поставлен и настроен, так, что в корневой директории документов сервера лежит файлик index.html, который замечательно показывается в браузере, когда вы набираете адрес http://127.0.0.1. Так же я учту, что где-то в дебрях папок сервера существует папочка «cgi-bin», в которой разрешен запуск скриптов.

Давайте проверим настройку сервера, а заодно и напишем небольшой скрипт. Скрипт наш будет обычным *.bat файлом. Предвижу вопросы – как? неужели? Да, это обычный командный файл, как уже говорилось выше спецификация CGI не делает различий между типами файлов, главное, чтобы сервер мог его запустить, а он в свою очередь, имел доступ к stdin/stdout и переменным окружения, bat-файл, пусть и не в полной мере, но для примера нас вполне устроит. Создадим файл примерно такого содержания:

@echo off
rem Заголовок апроса
echo Content-type: text/html
echo.
rem Тело запроса
echo "<html><body>Привет!<br>
echo "С запросом GET пришли данные: %QUERY_STRING%</body></html>

Файл назовем test.bat и поместим его в директорию для запуска скриптов, скорее всего это будет директория «cgi-bin». Следующее, что нам нужно будет сделать, это каким либо образом вызвать этот скрипт, в принципе, сделать это можно напрямую набрав в окошке адреса браузера примерно следующее «http://127.0.0.1/cgi-bin/test.bat», но давайте сделаем его вызов с нашей главной странички, заодно проверим работу метода GET. Создадим в корне сервера файл index.html со следующим содержанием:

<html><head>
<meta content="text/html; charset=windows-1251" http-equiv=Content-Type>
</head>
<form name="Test" method=get action="/cgi-bin/test.bat">
<center><table border=1 cellspacing=0>
<tr bgcolor="#e0e0e0"><td colspan=2><font face=arial size=2>
<center><b>Введите данные для передачи серверу:</b></center>
</font></td></tr>
<tr><td><font face=arial size=2>
<b>Данные:</b></font></td>
<td><font face=arial size=2>
<textarea rows="3" cols="55" warp="physical" name="data"></textarea>
</font></td></tr><tr bgcolor="#e0e0e0">
<td colspan=2><center>
<input type="submit" value="Послать!">
</center></td>
</tr></table></center></form></body></html>

Теперь при входе на сервер (http://127.0.0.1 в строке адреса браузера) должна появиться форма, наберите в ней что-нибудь и нажмите кнопку «послать», если все было сделано правильно, Вы увидите в окне браузера ответ нашего bat-скрипта. Теперь давайте посмотрим что же мы там намутили.

Как можно догадаться команда «echo» осуществляет вывод в stdout, первым делом мы передаем серверу заголовок нашего ответа – «echo Content-type: text/html». Это есть стандартный заголовок спецификации CGI, говорящий о том, что передавать мы хотим текст или документ html, существуют и другие заголовки. Очень важный момент – заголовок должен отделяться от тела ответа пустой строкой, что мы и делаем следующей командой «echo.». Дальше передается тело самого ответа – это обычный html-документ, в теле документа я для наглядности отображаю одну из переменных среды, переданной нам сервером – «QUERY_STRING», как уже говорилось при методе GET (а это именно наш случай) в этой переменной передаются все введенные пользователем данные, что мы и можем наблюдать в ответе скрипта. Вы могли заметить «кавычки не к месту» в последних 2-х строках файла, сразу после «echo», стоят они там из-за специфичности bat-файлов, как можно заметить тэги html обрамляются символами «<» и «>», в тоже время эти символы служат перенаправлением ввода/вывода в bat-файлах, а посему мы не можем их здесь свободно использовать.

Рекомендую немного побаловаться с подобными bat-скриптами, это бывает очень полезно, попробуйте посмотреть другие переменные окружения. Немного скажу, отступив от темы, на UNIX-системах языки командных интерпретаторов очень сильно развиты и грань между программированием на языке командного интерпретатора и программированием на «реальном» языке программирования весьма и весьма размыта в некоторых случаях, поэтому на UNIX-системах частенько простенькие скрипты пишутся именно на языках командных интерпретаторов, но windows-интерпретатор cmd.exe или, ранее, command.com явно слабоваты для этих целей.

Теперь перейдем к самой главной задаче этой статьи, к собственно написанию CGI-программы на ассемблере. В принципе, если учесть все вышесказанное о CGI мы можем сделать вывод о том, что требует CGI-интерфейс от нашей программы:

1.    Программа должна уметь читать стандартный поток ввода (stdin), чтобы получить доступ к данным, переданным методом POST;

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

3.    Из первых двух пунктов следует, то, что для того, чтобы сервер мог передать нашей программе что-либо в stdin, а она могла ему что-либо ответить в stdout CGI-программа должна быть консольным приложением;

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

Этого вполне достаточно для создания полноценного CGI-приложения.

Начнем с последнего пункта. Для получения доступа к переменным окружения Windows-приложения используется функция API «GetEnvironmentStrings», функция не имеет аргументов и возвращает указатель на массив переменных окружения (ИМЯ=ЗНАЧЕНИЕ) разделенных между собой нулем, массив закрывается двойным нулем, при запуске программы сервером в окружение программы помимо стандартных переменных добавляются специфические CGI-переменные, описанные выше, при запуске программы из командной строки вы их не увидите, естественно.

Для того, что бы писать что-то в stdout или читать из stdin сначала мы должны получить хэндлы этих потоков, делается это с помощью функции API «GetStdHandle», в качестве параметра функции передается одно из следующих значений:

STD_INPUT_HANDLE               - для stdin (стандартный ввод);

STD_OUTPUT_HANDLE           - для stdout (стандартный вывод);

STD_ERROR_HANDLE                        - для stderr.

Функция возвратит необходимый нам для операций чтения/записи хэндл. Следующее что нам необходимо делать это писать/читать эти потоки. Делается это обычными операциями чтения/записи файлов, т.е. ReadFile и WriteFile. Тут есть одна тонкость, можно подумать, что для этих целей можно использовать WriteConsole/ReadConsole, да это действительно справедливо для консоли и будет прекрасно работать, результаты, так же как и с WriteFile будут выводиться на консоль, но продолжаться это будет пока мы не запустим нашу программу как скрипт на сервере. Происходит это потому что, когда нашу программу запускает сервер хэндлы, возвращаемые функцией «GetStdHandle» уже не будут хэндлами консоли как таковыми, они будут хэндлами pipe, что необходимо для связи двух приложений.

Вот небольшой пример того, как должна выглядеть CGI-программа на ассемблере, думаю разобраться в ней не составит большого труда:>

.386
.model flat,stdcall
includelib import32.lib
.const
PAGE_READWRITE         =     4h
MEM_COMMIT             =     1000h
MEM_RESERVE            =     2000h
STD_INPUT_HANDLE       =     -10
STD_OUTPUT_HANDLE      =     -11

.data
hStdout         dd ?
hStdin          dd ?
hMem            dd ?
header:
                db 'Content-Type: text/html',13,10,13,10,0
start_html:
                db '<html><body><b>Окружение CGI-программы выглядит \
				    так:</b><br>',13,10,0
for_stdin:
                db '<b>STDIN программы содержит:</b><br>',13,10,0
end_html:

                db '</body></html>',13,10,0
nwritten        dd ?
toscr           db 10 dup (32)
                db ' - Тип файла',0
.code
_start:

        xor     ebx,ebx
        call    GetStdHandle,STD_OUTPUT_HANDLE
        mov     hStdout,eax
        call    GetStdHandle,STD_INPUT_HANDLE
        mov     hStdin,eax

        call    write_stdout, offset header
        call    write_stdout, offset start_html

        call    VirtualAlloc,ebx,1000,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE
        mov     hMem,eax
        mov     edi,eax
        call    GetEnvironmentStringsA
        mov     esi,eax
next_symbol:
        mov     al,[esi]
        or      al,al
        jz      end_string
        mov     [edi],al
next_string:
        cmpsb
        jmp     short next_symbol
end_string:
        mov     [edi],'>rb<'
        add     edi,3
        cmp     byte ptr [esi+1],0
        jnz     next_string
        inc     edi
        stosb
        call    write_stdout, hMem
        call    write_stdout, offset for_stdin

        call    GetFileSize,[hStdin],ebx
        mov     edi,hMem
        call    ReadFile,[hStdin],edi, eax,offset nwritten, ebx
        add     edi,[nwritten]
        mov     byte ptr [edi],0
        call    write_stdout, hMem
        call    write_stdout, offset end_html
        call    VirtualFree,hMem
        call    ExitProcess,-1

write_stdout    proc bufOffs:dword
        call    lstrlen,bufOffs
        call    WriteFile,[hStdout],bufOffs,eax,offset nwritten,0
        ret
write_stdout    endp
extrn   GetEnvironmentStringsA:near
extrn   GetStdHandle:near
extrn   ReadFile:near
extrn   WriteFile:near
extrn   GetFileSize:near
extrn   VirtualAlloc:near
extrn   VirtualFree:near
extrn   ExitProcess:near
extrn   lstrlen:near
ends
end             _start

Исполняемый файл строится командами:

tasm32.exe /ml test.asm

tlink32.exe /Tpe /ap /o test.obj

Не забудьте, что программа должна быть консольной.

Архив с программой здесь.

Вызывать эту программу можно используя вышеописанную html-форму, нужно только поменять имя test.bat в форме на test.exe и скопировать его в /cgi-bin/ соответственно, при том можно выставить в методе запроса POST, программа его обрабатывает.

Еще хочу отметить, что можно вызывать программу и по-другому, можно создать в каталоге cgi-bin файл например test.cgi с одной единственной строчкой «#!c:/_путь_/test.exe» и вызывать в запросах его, а сервер в свою очередь будет читать первую его строчку и запускать exe-файл, для этого необходимо, чтобы в настройках http-сервера было прописано расширение *.cgi как расширение для скриптов. При таком подходе сервер запустит нашу программу с командной строкой «test.exe путь_к_test.exe» это имеет несколько плюсов – первое, это то, что человек, запускающий наш скрипт не будет даже догадываться на чем скрипт написан, второе – так-как нам передается имя файла с нашей строчкой мы можем например дописать в этот файл какие-либо настройки для нашего скрипта, что упрощает отладку, кстати именно так работают все интерпретаторы – вы успели заметить, что во всех perl/php/итд программах, присутствует подобная строка – указывающая на сам командный интерпретатор. Так вот сервер при запуске cgi-программы, если расширение программы прописано у него как скрипт в настройках читает первую строку файла, и если она оказывается описанного выше формата, то запускает указанную в строчке программу с именем этого файла ч/з пробел, допустим что в строчке указан интерпретатор перла, он получив такой подарок начинает его выполнение, т.к. комментарий в перле это символ «#», то первую строчку он пропускает и идет дальнейшее выполнение скрипта, в общем штука удобная.

Вот в общем и все о чем я хотел написать, не знаю насколько это все окажется Вам полезным, но скажу что у меня работает сервер интрасети используя скрипты на ассемблере. Каюсь, больших оснований делать это не было, но все же я сделал это сначала просто из эстетических соображений и некоторой не охоты учить перл/php или что-то еще. НО я никоим образом не отговариваю Вас учить перл, а наоборот скажу что сделать это нужно, и даже очень нужно, это я понял позже, но все же считаю, что на сильно загруженных серверах, где скорость выполнения, загрузки  и объем памяти занимаемый приложением играет решающую роль cgi-скрипты, написанные на ассемблере займут свое достойное место.

2002-2013 (c) wasm.ru