Программирование CGI в Delphi и Kylix (статья)

== Введение ==
В настоящем руководстве объясняется, как программировать CGI на Delphi и Kylix. Автор будет рад Вашим замечаниям и пожеланиям! 
Уточнения:
* Для работы с CGI вам потребуется Web-сервер (для Delphi - под Windows, а для Kylix - под Linux)...
Примечание:
* Если вы планируете использовать ISAPI/NSAPI DLL, то лучше будет программировать на Delphi 5/6; однако настоящее руководство остается весьма полезным, если Вы желаете разобраться в том, как функционирует CGI.
* Автор тестировал свои программы на сервере Lotus Domino под NT, и на сервере Apache под Mandrake 7.0 (linux).
* Автор использовал Delphi 2.0, однако это руководство применимо и для Delphi 3,4,5, 6... и Kylix ! 
=== Основные понятия ===
Ссылки на CGI-программу:
На HTML-странице (или непосредственно в строке URL браузера) вы помещаете ссылку на вашу программу. Вот несколько примеров ссылок:
Простая ссылка:
 <a href="/cgi-bin/program.exe">
Запрос вывода изображения:
 <img src="/cgi-bin/program.exe">
Форма с запросом типа GET:
 <form method=GET action="/cgi-bin/program.exe"> ... </form>
Форма с запросом типа POST:
 <form method=POST action="/cgi-bin/program.exe"> ... </form>
Прямое обращение по URL:
 http://www.tonserver.fr/cgi-bin/program.exe
Что такое cgi-bin:
cgi-bin - это псевдоним каталога на сервере, который указывает на реальный каталог, в котором размещены все CGI программы.
Например:
* Под Windows:
 c:\internet\delphi\cgi
* Под Linux:
 /home/httpd/cgi-bin
Запуск CGI-программ:
Когда пользователь кликает на ссылке, указывающей на CGI-программу, сервер запускает данную программу и ожидает от нее ответа.
Ответ CGI-программы:
Самым простым вариантом CGI программы может служить консольное приложение {$apptype console}, которое похоже на DOS-программу, однако работает под Windows 95/NT, или под Linux.
Эта возможность позволяет тестировать CGI-программу локально, выводя результат работу на экран.
Пример простейшей CGI-программы:
program ExampleCGI;
{$apptype console}
begin
 WriteLn('Content-type: text/html');
 WriteLn;
 WriteLn('Всем привет !');
end.
Разберем строки, выводимые программой:
:1) WriteLn('Content-type: text/html'); - Content-type - это описание типа выводимых данных (в данном случае - текста HTML)
:2) WriteLn; - Вывод пустой строки ОБЯЗАТЕЛЕН, для того, чтобы отделить "заголовок" документа от выводимого далее содержимого этого документа.
:3) WriteLn('Всем привет !'); - Здесь выводится собственно тело документа, т.е. то, что мы увидим, если нажмем в браузере "Файл - Просмотр в виде HTML"
Для обращения к программе в строке адреса в браузере необходимо набрать:
 http://ваш_сервер/cgi-bin/ExampleCGI
Использование Writeln:
Все, что выводится командой WRITELN, направляется в "устройство стандартного вывода" STDOUT и отправляется сервером в браузер пользователя.
Интересно, что под Windows можно написать CGI-программу даже с помощью .BAT-файла!
 @ECHO OFF
 ECHO content-type: text/html
 ECHO.
 ECHO ^<HTML^>^<HEAD^>^<TITLE^>^</TITLE^>^</HEAD^>^<BODY^>
 ECHO Всем привет !
 ECHO ^</BODY^>^</HTML^>
Обратите внимание, что специальные символы, используемые в DOS (такие, как "<", ">", "&",...), необходимо предварять знаком "^".
Не забывайте об этом при написании CGI с .BAT файлами...
== Передача параметров ==
=== Методы GET и POST ===
Существует по крайней мере два метода передачи параметров CGI-программе.
 <form method=GET action="program.exe">
 <form method=POST action="program.exe">
Чтобы определить, каким именно методом CGI-программе переданы параметры, достаточно в вашей программе проверить переменную среды REQUEST_METHOD.
Ниже приведена функция, с помощью которой можно получить значение переменной среды окружения:
function getvar(varname: string): string;
{$IFDEF LINUX}
begin
 Result := GetEnv(PChar(varname));
end;
{$ELSE}
var
 buffer: array[0..1024] of Char;
 size: Integer;
begin
 size := GetEnvironmentVariable(PChar(varname), buffer, SizeOf(buffer));
 if size = 0 then
 Result := ''
 else
 Result := String(buffer);
end;
{$ENDIF}
Автор предпочитает работать не с массивами, а со строками, поэтому результат функции преобразовывается в строку... 
Теперь посмотрим, как определить значение переменной среды под DOS в .BAT файле:
 @ECHO OFF
 ECHO content-type: text/html
 ECHO.
 ECHO ^<HTML^>^<HEAD^>^<TITLE^>^</TITLE^>^</HEAD^>^<BODY^>
 ECHO REQUEST_METHOD=%REQUEST_METHOD%
 ECHO ^</BODY^>^</HTML^>
Обратите внимание, что специальные символы, используемые в DOS (такие, как "<", ">", "&",...), необходимо предварять знаком "^".
Таким образом, если мы обратимся к функции в виде GetVar('REQUEST_METHOD'), то получим в виде строки метод, которым были переданы параметры: 'GET' или 'POST'.
Согласно Спецификации CGI, параметры могут быть прочитаны:
* Из переменной окружения QUERY_STRING для метода GET 
* Из стандартного ввода (STDIN) с помощью процедуры ReadLn для метода POST
Метод POST используется в тех случаях, когда необходимо передать большое количество параметров или большой объем данных. При использовании же метода GET для хранения всех передаваемых параметров используется переменная среды окружения, а она, как вы понимаете, не резиновая, так что ее максимального размера в некоторых случаях может не хватить...
=== Метод GET и переменная QUERY_STRING ===
Переменная среды окружения QUERY_STRING содержит список имен и значений параметров, переданных из формы... Но сначала рассмотри код HTML:
 <form method="GET" action="program.exe">
 <input type=text name="toto" value="titi">
 <input type=submit value="GO">
 </form>
Кликнув на "GO" (здесь кликать не надо, это просто пример!), вы запускаете на сервере программу "program.exe" передавая серверу запрос в виде:
 http://www.ваш_сервер/cgi-bin/program.exe?toto=titi
Вы видите, что сразу за именем программы следует знак вопроса и передаваемый в программу параметр. В переменную QUERY_STRING как раз и будет помещено все, что находится после символа "?". Заметим, что точно так же можно задать параметры и в обычной ссылке:
 <a href="http://www.ваш_сервер/cgi-bin/program.exe?toto=titi">...</a>
И последнее уточнение: если запрос содержит несколько параметров, то они отделяются друг от друга амперсандом &. Кроме того, некоторые символы в значениях параметров (например, "&") должны быть представлены в шестнадцатеричной форме %hh, где "hh" - шестнадцатеричный код символа в таблице ANSI. Например, символ амперсанда "&" должен быть представлен в виде "%26".
Представьте, что вам требуется на сайте yahoo.com найти результаты поиска по ключевым словам cgi и delphi, для чего в окне поиска вы вводите "cgi + delphi".
Тогда в результате вашего запроса будет сгенерирован следующий URL:
 http://search.yahoo.com/bin/search?p=cgi+%2B+delphi
Тем самым, вы обратитесь к программе "search" и зададите значение параметра "p" равным "cgi + delphi", при этом символ "+" будет автоматически заменен браузером на "%2B", а пробелы - на "+".
=== Метод POST ===
Для получения данных, переданных по методу POST, необходимо читать данные из "устройства стандартного ввода" STDIN. Размер данных, переданных по этому методу, помещается сервером в переменную окружения CONTENT_LENGTH.
Посмотрим, как это выглядит в Delphi:
 // Получение переданных параметров
 if GetVar('REQUEST_METHOD')='POST' then
 begin
 parmstring := GetVar('CONTENT_LENGTH');
 if parmstring <> then
 begin
 size := StrToInt(parmstring);
 SetLength(parmstring, size);
 for i := 1 to size do
 Read(parmstring[i]);
 end;
 end
 else
 parmstring := GetVar('QUERY_STRING');
В заключение я предлагаю вашему вниманию маленькую CGI-программу, которая просто выводит то, что происходит на сервере:
program log;
{$apptype console}
// прикручивание к Linux попробуйте сделать сами :)
uses
 windows, sysutils;
var
 i: Integer;
 s: string;
 p: PChar;
 fLog: TextFile;
begin
 AssignFile(fLog, 'c:\temp\log.txt');
 Rewrite(fLog);
 WriteLn('Content-Type: text/html');
 WriteLn();
 WriteLn('<html><head><title>Dump CGI</title></head><body>');
 WriteLn('Dump CGI:');
 WriteLn('<a href=#Parms>Параметры программы</a>');
 WriteLn('<a href=#Query>Параметры CGI</a>');
 WriteLn('<a href=#Env>Переменные окружения</a>');
 WriteLn('<a href=#Info>Дополнительная информация о CGI</a>');
 WriteLn('');
 WriteLn('<a name=Parms>ParamCount=', IntToStr(ParamCount), '');
 WriteLn(fLog, 'ParamCount=', IntToStr(ParamCount));
 for i := 0 to ParamCount do
 begin
 WriteLn('', ParamStr(i));
 WriteLn(fLog, '-', ParamStr(i));
 end;
 // Стандартный Ввод
 WriteLn(fLog, 'Input :');
 WriteLn('StdInput:');
 if not Eof(Input) then
 begin
 Read(Input, s);
 WriteLn('', s);
 WriteLn(fLog, s);
 end;
 Writeln(fLog, 'QUERY_STRING=', ParmString);
 WriteLn('<a name=Env>Переменные окружения:');
 p := GetEnvironmentStrings;
 while StrLen(p) <> 0 do
 begin
 WriteLn('', p);
 WriteLn(fLog, ':', p);
 p := StrEnd(p);
 Inc(p);
 end;
 WriteLn('');
 WriteLn('<a name=Info><a href="http://www.multimania.com/tothpaul">');
 WriteLn('Дополнительная информация о CGI</a>');
 WriteLn('</body></html>');
end.
; Примечание: Кроме стандартного вывода информация параллельно выводится еще и в лог-файл.
== Переадресация ==
=== Заголовок HTTP-ответа ===
Мы уже знаем, что CGI-программа отсылает серверу заголовок, не отображаемый браузером:
 WriteLn('Content-Type: text/html');
 WriteLn();
=== Вид заголовка для переадресации ===
Следует отметить, что в заголовке может быть приведено множество других директив, в частности, CGI-программа может переадресовать запрос на другую страницу...
Для переадресации достаточно вывести заголовок в следующем виде:
 WriteLn('Location: redirection.htm');
 WriteLn();
Кроме того, ваш сервер автоматически добавляет в этот заголовок еще и свои собственные сообщения.
Допустим, вы запрашиваете в браузере URL http://yahoo.com. В этом случае вы получите от сервера следующий ответ:
 HTTP/1.0 302 Found
 Location: http://www.yahoo.com
Получив такой заголовок, браузер перезапрашивает у сервера новый URL http://www.yahoo.com, и в ответ получает следующее:
HTTP/1.0 200 OK
Content-Length: 9332
Expires: Wed, 18 Mar 1998 08:00:03 GMT
Content-Type: text/html
<nowiki><html><head><title>Yahoo!</title>
<base href="http://www.yahoo.com/"></head>
<body>
<form action="http://search.yahoo.com/bin/search">
<a href="/bin/top3">
<img width=460 height=59 border=0 usemap="#top3" ismap src="http://us.yimg.com/i/main32.gif" alt="Yahoo!"></a>
 
 
...
</nowiki>
Таким образом происходит просто переадресация на другую страницу!
И последнее замечание: вам не нужно заботиться самим о выводе строк типа "HTTP/1.0...", и "Content-Length: ...", поскольку это делает автоматически сам сервер.
== Вывод изображений ==
=== Заголовок HTTP-ответа для HTML-страниц ===
Мы уже знаем, что для сообщения браузеру, что передаваемый документ является HTML-документом, CGI-программа выводит специальный заголовок, не отображаемый браузером:
 WriteLn('Content-Type: text/html');
 WriteLn();
=== HTTP-заголовок для изображений ===
Точно таким же образом можно с успехом указать и другой тип данных! Например, для вывода изображения в формате GIF достаточно вывести следующее:
 WriteLn('Content-Type: image/gif');
 WriteLn();
Таким образом мы сообщаем браузеру, что далее будет следовать именно изображение...
=== Передача двоичных данных ===
Для начала давайте разберемся, как отправить двоичные данные в STDOUTPUT.
Я написал две процедуры: первая выводит поток TSTREAM в STDOUTPUT, а вторая выводит двоичный файл в выходной поток:
// Процедура вывода потока в STDOUTPUT.
// Попробуйте самостоятельно переделать ее для Kylix...
procedure WriteStream(stream: TStream);
var
 OutStream: THandleStream;
begin
 Flush(output); // для передачи заголовка мы используем обычный WRITELN...
 // здесь используется код из программы
 // DCOUNTER for Delphi 3 by Dave Wedwick (dwedwick@bigfoot.com)
 OutputStream := THandleStream.Create(GetStdHandle(STD_OUTPUT_HANDLE));
 Stream.SaveToStream(OutputStream);
 OutputStream.Free;
end;
// Процедура для передачи двоичного файла
procedure WriteFile(FileName: string);
var
 fs: TFileStream;
begin
 fs := TFileStream.Create(FileName, fmOpenRead);
 try
 WriteStream(fs);
 finally
 fs.Free();
 end;
end;
=== Передача GIF файлов ===
Теперь нам осталось только создать (или взять готовый) GIF файл и вывести его!
procedure WriteGIF(FileName: string);
begin
 WriteLn('Content-type: image/gif');
 WriteLn;
 WriteFile(FileName);
end;
== Защита паролем ==
=== Статус в HTTP-заголовке ===
В дополнение к строкам заголовка, которые формирует CGI-программа, сервер выводит дополнительную строку, в которой сообщает о статусе обработки полученного запроса. Например, если запрос обработан успешно, сервер выдает следующую строку в заголовке:
 HTTP/1.0 200 OK
=== Запрос на авторизацию ===
Если вы попробуете запустить без изменений мой "web browser" (см. страницу delphi), то вы увидите, что полученный HTTP заголовок несколько отличается от обычного:
 HTTP/1.0 401 Unauthorized
 Content-type: text/html
 WWW-Authenticate: Basic realm="/MyRealm"
 
 <html><head><title>401 Unauthorized</title></head><body>
 Для доступа к этой странице требуется пароль!
 </body></html>
Здесь сервер отсылает предупреждение 401 Unauthorized для того, чтобы ваш браузер знал, что он обратился к защищенной странице. Тем самым, сервер предлагает вам ввести пароль и логин в строке уточнения (realm= "/MyRealm"). Если вы предоставите серверу действительный логин и пароль, то сервер откроет доступ к данной странице. В противном случае браузер будет получать вместо нужной страницы предупреждение с кодом 401.
=== Формируем запрос на авторизацию ===
Теперь мы знаем все, что нужно, чтобы наша CGI-программа могла запросить от пользователя пароль для доступа:
program CheckPWD;
{$apptype console}
begin
 WriteLn('HTTP/1.0 401 Unauthorized');
 WriteLn('Content-type: text/html');
 WriteLn('WWW-Authenticate: Basic realm="/Check"');
 WriteLn;
 WriteLn('Для доступа к этой странице требуется пароль!');
end;
... и, к нашему удивлению, убеждаемся, что такой вариант не работает! Мы получаем либо внутреннюю ошибку сервера, либо браузер выдает полученный текст как обычную страницу, не понимая, что от него требуют ввести пароль!
Спокойно, без паники! Так и должно быть. Я уже говорил, что сервер автоматически дополняет HTTP-заголовок своими сообщениями, и браузер после выполнения программы CheckPWD получит следующее:
 HTTP/1.0 200 OK
 HTTP/1.0 401 Unauthorized
 Content-type: text/html
 WWW-Authenticate: Basic realm="/MyRealm"
 
 Для доступа к этой странице требуется пароль!
Первая строка в заголовке говорит о том, что запрос обработан успешно (спасибо серверу). Поэтому браузер и не требует ничего от пользвателя.
У меня есть несколько вариантов выхода из этой ситуации, и один из вариантов заключается в том, что в соответствии с требованиями CGI, имя файла CGI-программы должно начинаться с "nph-", если она сама должна полностью формировать HTTP-заголовок.
Таким образом, достаточно переименовать вышеприведенную программу в "NPH-CHECKPWD.EXE", и все заработает!
Итак, мы добились, чтобы наша CGI-программа заставляла браузер потребовать от пользователя предоставить информацию об авторизации!
=== WWW-Authenticate ===
Получив запрос на авторизацию, браузер выводит на экран диалог для ввода пользователем логина и пароля и отправляет их на сервер в виде строки
 WWW-Authenticate: Basic realm="/MyRealm"
В результате, CGI-программа получит от сервера строку авторизации в виде значения переменной среды окружения HTTP_AUTHORIZATION.
 HTTP_AUTHORIZATION=Basic dXNlcjpwYXNzd29yZA==
В этой строке как раз и записан введенный логин и пароль, но только не в открытом, а в закодированном виде... Для кодировки строки авторизации используется формат Base64.
Сделаем небольшое отступление об основаниях систем счисления (как всегда - математика рулит!):
* Люди привыкли считать по основанию 10 (т.е. в 10-чной системе счисления): 0..9 
* В информатике часто используется 16-ричное представление: 0..9,'A'..'F' 
* В интернет очень широко используется представление в системе счисления с основанием 64: 0..9,'A'..'Z','a'..'z','+','/'
В файле LOGIN.ZIP вы найдете пример использования модуля Base64, который осуществляет кодирование и декодирование строк по основанию 64.
Кстати, вышеприведенный текст расшифровывается с помощью Base64 очень просто: "user:password"
Итак, теперь мы можем написать CGI-программу, которая будет либо показывать запрошенную информацию, либо требовать от пользователя авторизации.
Еще одной альтернативой является переадресация на другую страницу, если авторизация не подтверждена.
Практической реализацией изложенных выше принципов является программа login.
; Внимание: Не забудьте переименовать файлы login.exe и login.ini из этого в nph-login.exe и npg-login.ini !
В завершение я предлагаю вашему вниманию программу "CGI Web Browser". Эта программа является консольным приложением Delphi, позволяющим просматривать диски и каталоги на сервере и загружать файлы. Для разрешения доступа к дискам сервера программа требует ввода логина и пароля. (Не забудьте поменять логин/пароль в исходном коде!)
== Куки (Cookies) ==
=== Введение ===
Давайте вспомним, каким образом мы можем передавать CGI-программе параметры.
При использовании метода GET параметры передаются в строке URL.
При использовании метода POST параметры передаются отдельно и не видны пользователю.
(отметим также, что в Internet Explorer 3 есть ошибка, из-за которой он ТЕРЯЕТ параметры при нажатии кнопки "Обновить").
Таким образом, нам не хватает возможности хранить информацию о передаваемых параметрах.
=== Что такое Cookie ? ===
"Cookie" - это небольшая порция информации, которая сохраняется на компьютере пользователя и привязана к конкретному URL. Когда браузер обращается к данному URL, он передает на удаленный сервер этот блок информации. В результате на сервере будет сформирована строка окружения HTTP_COOKIE, в которой содержится список всех cookies, которые относятся к данному URL.
Перменная окружения HTTP_COOKIE будет содержать пары имен/значений параметров, разделенных символом ";" в следующем виде:
 имя1=значение1;имя2=значение;...
Таким образом мы можем задавать сразу несколько параметров в одном cookie для данного URL.
=== Как создать Cookies ? ===
Давайте разберемся, как это делается! Для создания куков достаточно использовать HTTP-заголовок. Вспомним, каким образом задается тип передаваемого документа в заголовке:
 Content-type: text/html
Точно таким же образом в заголовке задаются и cookie! Просто добавляем еще одну строку:
 Content-type: text/html
 Set-Cookie: Name=Value
Таким образом, к примеру, можно сохранить на компьютере пользователя его имя и пароль, необходимые для входа на защищенный сайт или защищенную страничку. Многие сайты запрашивают у пользователя имя и пароль, передают их по методу POST, и затем сохраняют на компьютере полдьзователя в виде cookie.
=== Пример использования Cookies ===
По просьбам читателей я сделал программу Cook, демонстрирующую, как использовать cookies для авторизации пользователя.
В архиве COOK.ZIP находится самая последняя версия моего модуля CGI и модуль Base64.
== Работа с Базами Данных ==
Как это делается ?
Доступ базам данных, разумеется, имеет отношение не только к CGI... Но в этом контексте надо учитывать ряд особенностей.
Поскольку CGI-программа запускается и выполняется на сервере, то она ДОЛЖНА обязательно завершать свою работу (и чем быстрее - тем лучше) для того, чтобы сервер и браузер пользователя считали, что запрос завершен... При каждом новом обращении к CGI происходит новый произвольный запрос к базе данных.
В связи с этим, для получения удовлетворительного результата нам нужно максимально оптимизировать все операции с базой данных - открытие, обработку и корректное закрытие базы.
В качестве рабочего примера обработки базы данных из CGI я предлагаю программу ABook. Эта программа использует ODBC-драйвер для работы с базой данных MS Access. При каждом обращении программа открывает базу, обрабатывает запрос, и закрывает базу...
Хотя я и не тестировал эту программу на больших базах данных, но хочу заметить, что в любом случае открытие базы данных MS Access с помощью ODBC производится несравненно быстрее, чем при использовании BDE!
Для более серьезных применений несомненно лучше будет использовать "сервер баз данных", который постоянно крутится на сервере. В этом случае CGI-программа будет тратить время только на отправку запроса серверу, не заботясь об открытии и закрытии базы данных...
В качестве дальнейшего усовершенствования можно попытаться определять сессии (например, с помощью Cookies) для того, чтобы сохранять параметры между двумя запросами...
== Частые Вопросы и Ответы ==
1) Настройка CGI для IIS
Запустите программу
 Пуск/Программы/Microsoft Internet Server/Служба Управления
Кликните два раза на службе WWW, и выберите закладку "Каталоги":
 Каталог Алиас Адрес Ошибка 
 C:\InetPub\wwwroot <базовый каталог>
 C:\InetPub\scripts /Scripts
 C:\WINNT\System32\inetsrv\iisadmin /iisadmin
Кликните на Добавить, укажите каталог, в котором будут содержаться CGI-программы (например C:\DELPHI).
Алиас виртуального каталога, обычно называемый "/cgi bin", заменяет права доступа для чтения на права доступа для "Выполнения".
 Каталог Алиас Адрес Ошибка 
 C:\InetPub\wwwroot <базовый каталог>
 c:\delphi /cgi-bin
 C:\InetPub\scripts /Scripts
 C:\WINNT\System32\inetsrv\iisadmin /iisadmin
Теперь нам достаточно поместить наши CGI-программы в каталог C:\DELPHI и обращаться к ним примерно так:
 http://ваш_сервер/cgi-bin/program.exe
Если ваша NT выдает ошибку прав доступа на конкретном документе, кликните на этом документе, и проверьте, разрешен ли доступ...
2) Как избавиться от запроса СОХРАНИТЬ/ВЫПОЛНИТЬ при клике на ссылку вида <a href="/cgi-bin/programm.exe"> ?
Для того, чтобы браузер не спрашивал у вас, надо ли сохранить или выполнить вашу CGI-программу, необходимо обязательно размещать выполняемые программы не где попало, а именно в том каталоге, который вы указали серверу в качестве каталога CGI...
Если вы установили web-сервер на локальный компьютер (localhost), то обращаться к нему нужно следующим образом:
 http://127.0.0.1/cgi-bin/programm.exe
Перевод с французского: Valery Votintsev
Author: Paul TOTH
Source: http://forum.sources.ru
ID: 03417
Category:Связь
Category:Web
Category:Статьи
Category:Учебники