Поиск по этому блогу

среда, 19 января 2011 г.

CGI на Lazarus

CGI на Lazarus. Минимальное приложение. POST & GET запросы
Windows / Lazarus 0.9.28.2 beta / fpc 2.2.4
Механизм взаимодействия cgi

1. При получении запроса обозревателя к cgi сервер запускает приложение, передает ему данные из командной строки запроса
2. cgi формирует ответ и помещает его в выходной поток
3. сервер посылает ответ, используя http протокол обратно обозревателю

недостатки cgi - невысокая скорость обработки, повышенная загрузка web сервера, т.к. в случае паралелльной обработки сервер запускает отдельный процесс, для каждого процесса - копию модуля расширения памяти компьютера


для передачи/получения параметров используются стандартный входной/выходной потоки (stdin/stdout)
для передачи параметров и получения системной информации могут использоваться переменные окружения

Само CGI приложение - это ни что иное, как обычное консольное приложение (Win32 CONSOLE). Вот только устройством ввода для него служит строка браузера (URL), в котором содержаться get запросы или форма на html странице с кнопочкой submit, которая отправляет post запросы.
Запустить cgi приложение можно просто набрав в строке браузера адрес, который приведет нас к cgi приложению
http://127.0.0.1:8081/cgi/cgifirst.exe
Можно компилировать cgi приложения с расширением cgi, а можно с расширением exe, это не принципиально.

Настройки Apache для запуска cgi программ: (http://httpd.apache.org/docs/2.2/howto/cgi.html)
Файл конфигурации сервера httpd.conf
При старте должны загружаться модули:     * mod_alias    * mod_cgi
По умолчанию путь к cgi модулям находится в директории [Путь куда установлен Apache]/cgi-bin
Директива ScriptAlias  позволяет задать псевдоним для директории со скриптами
ScriptAlias /cgi/ "C:/Apache/test.pfr/www/cgi/"

Опции
specify that CGI execution was permitted in a particular directory:
<Directory /usr/local/apache2/htdocs/somedir>
Options +ExecCGI
</Directory>
Директива AddHandler сообщает серверу, что файлы с таким расширением (.cgi .pl) будут опознаны как CGI программы
AddHandler cgi-script .cgi .pl

Пример. Определяем путь к приложениям для виртуального хоста.
Настройка виртуального хоста у нас находится в файле httpd-vhosts.conf.
В этом файле мы определили виртуальный хост, который будет слушать 8081 порт,
директория с документами будет располагаться в "C:/Apache/test.pfr/www",
а cgi приложения будут располагаться здесь "C:/Apache/test.pfr/www/cgi/"
(для linux'ов )
NameVirtualHost *:8081
#
# VirtualHost example:
# Almost any Apache directive may go into a VirtualHost container.
# The first VirtualHost section is used for all requests that do not
# match a ServerName or ServerAlias in any <VirtualHost> block.
#
<VirtualHost *:8081>
    ServerAdmin webmaster@dummy-host2.localhost
    DocumentRoot "C:/Apache/test.pfr/www"
    ServerName test.pfr
    ErrorLog "c:/apache/test_error.log"
    CustomLog "c:/apache/test_access.log" common
    ScriptAlias /cgi/ "C:/Apache/test.pfr/www/cgi/"
</VirtualHost>

Разбираемся с переменныеми окружения.
Для получения от web сервера служебной информации, параметров http запроса служат переменные окружения.
REQUEST_METHOD - способ передачи параметров (POST или GET)
PATH_INFO      содержит подстроку передаваемых параметров после символа "/"
QUERY_STRING      Строка переданных серверу параметров
CONTENT_LENGTH      Длина содержимого, например, text/html
CONTENT_TYPE     MIME-тип содержимого, например, text/html

Полный список переменных можно  легко нагуглить

POST && GET
Чтобы определить, каким именно методом CGI-программе переданы параметры, в программе надо прочитать переменную среды REQUEST_METHOD.
Параметры могут быть прочитаны:
    1. Из переменной окружения QUERY_STRING для метода GET
    2. Из стандартного ввода (STDIN) с помощью процедуры ReadLn для метода POST


Минимальное приложение из демо лазаря (http://wiki.lazarus.freepascal.org/CGI_Web_Programming)
эхо, читает данные и отправляет их обратно клиенту.
   1. Setting a cookie
   2. Outputting the content-type (ie make it put out legal text for HTTP)
   3. Reading Cookies
   4. Reading form data via GET
   5. Reading form data via POST
создаем обычное консольное приложение, где и пишем:
program mini;
uses dos;
var
  a:string;
  c:char;
begin
  // set a cookie (must come before content-type line below)
  // don't forget to change the expires date
writeln('Set-cookie:widget=value; path=/; expires= Mon, 29-Dec-2010 8:37:00 GMT');
  // output legal http page
  writeln('Content-Type:text/html',#10#13);
  // demonstrate get cookies
  a:= GetEnv('HTTP_COOKIE');
  writeln('cookies:',a);
  // demonstrate GET result
  a:='';
  a:= GetEnv('QUERY_STRING');
  writeln('GET: ',a);
  // demonstrate POST result
  a:='';
  while not eof(input) do
  begin
     read(c);
     a:= a+c;
  end;   
  writeln('POST: ',a);
end.

Результат работы http://127.0.0.1:8081/cgi/mini.exe?test=1234 - отправили get запрос
Set-cookie:widget=value; path=/; expires= Mon, 29-Dec-2010 8:37:00 GMT Content-Type:text/html cookies:
    GET: test=1234 POST:

********************************************************************************
GET запросы
Помещаются в URL запроса
Формат запроса: GET                               сценарий?параметры
Переменные окружения:
QUERY_STRING содержат переданные значения и параметры
REQUEST_METHOD=GET. Метод запроса
Полезные переменные
'SCRIPT_NAME' - содержит URL адрес выполняемого модуля (без подстроки параметров)
пример
    http://127.0.0.1:8081/cgi/cgifirst.exe/name - исходная строка
    /cgi/cgifirst.exe - SCRIPT_NAME
'PATH_INFO' - содержит подстроку передаваемых параметров после символа "/"
пример
    http://127.0.0.1:8081/cgi/cgifirst.exe/name - исходная строка
     /name - PATH_INFO

Пишем CGI приложение на Lazarus, fcl-web
Открываем Lazarus, создаем новую программу, вписываем туда следующий код, сохраняем как cgifirst.pp, компилируем программу fpc cgifirst.pp или из самой IDE (Ctrl-F9)
за данные, которые отправляет сервер отвечает класс
TRequest
за данные, которые CGI отправляет клиенту отвечает класс
TResponse
                          
program cgifirst;
{$mode objfpc}{$H+}
uses
  Classes, SysUtils, httpDefs, custcgi;
Type
{ TCGIApp }
TCGIApp = class(TCustomCGIApplication)
public
      Procedure HandleRequest(ARequest: Trequest; AResponse: TResponse);override;
end;

{ TCGIApp }
procedure TCGIApp.HandleRequest(ARequest: Trequest; AResponse: TResponse);
begin
  inherited HandleRequest(ARequest, AResponse);
// пишем клиенту
  AResponse.Content := 'Hello Word !'; // отправляем клиенту приветственное сообщение
end;

begin
     with TCGIApp.create(nil) do
     try
        Initialize;
        Run;
     finally
        Free;
     end;
end.
кладем exe файл в директорию C:/Apache/test.pfr/www/cgi/ и запускаем браузер, в командной строке которого набираем:
http://127.0.0.1:8081/cgi/cgifirst.exe

вуаля! перед нами надпись "Hello Word !" во всей своей красе :)

Теперь напишем приложение, которое реагирует на get запросы
GET запросы передаются в строке URL адреса, например
http://127.0.0.1:8081/cgi/cgifirst.exe?test=1234
часть строки после вопроса test=1234 - и есть get запрос
в cgi приложении мы получаем ее с помощью переменной окружения QUERY_STRING
Теперь наша программа выглядит так:

program cgifirst;
{$mode objfpc}{$H+}
uses
  Classes, SysUtils, httpDefs, custcgi, cgiModules;
Type
{ TCGIApp }
TCGIApp = class(TCustomCGIApplication)
private

public
      Procedure HandleRequest(ARequest: Trequest; AResponse: TResponse);override;
end;

{ TCGIApp }
procedure TCGIApp.HandleRequest(ARequest: Trequest; AResponse: TResponse);
var
 response_text: TStringList;
begin
  inherited HandleRequest(ARequest, AResponse); 
  AResponse.Content := 'You get variable is ' + EnvironmentVariable['QUERY_STRING'];
end;

begin
     with TCGIApp.create(nil) do
     try
        Initialize;
        Run;
     finally
        Free;
     end;
end.
Проверяем в браузере, вводим
http://127.0.0.1:8081/cgi/cgifirst.exe?test=1234
получаем: You get variable is test=1234
   
Можно анализировать запрос, который содержится в url строке и с помощью переменной PATH_INFO
Например такой запрос http://127.0.0.1:8081/cgi/cgifirst.exe/test
можно проанализировать следующим образом
var := EnvironmentVariable['PATH_INFO'];
if strcomp(var, '/test') <> 0
then... else ...
Как правило, такого рода переменные разработчики компонентов помещают в Action List, в некоторых компонентах есть даже событие OnAction.

Еще мы можем загрузить html файл (сформированный нами заранее) с диска, в компонентах fcl-web это выглядит следующим образом.
  AResponse.ContentType := 'text/html;charset=utf-8'; // указываем тип содержимого
  AResponse.Contents.LoadFromFile(ExtractFilePath(ParamStr(0)) + 'mainpage.html'); // грузим шаблон из файла
  Handled := True;

*******************************************************************************
Часть 2. POST запросы

POST помещает данные не в URL, а в тело запроса.
В программе мы получаем их из стандартного потока ввода (STDIN). При считывании входящего потока программа не должна пытаться читать больше чем указано в переменной CONTENT_LENGTH. (В консольных программах обычно поток ввода - это клавиатура или файл, а поток вывода - это консоль или экран)

Формат запроса: POST
Переменные окружения:
REQUEST_METHOD = POST.
CONTENT_LENGTH - содержит длину данных (в bytes)


Рисуем форму, которая будет отправлять POST запросы
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//RU">
<html>
 <head>
  <title>Заголовок</title>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8"> 
  <meta name="resourse-type" content="document">
 </head>
<body>
 <FORM ACTION="http://127.0.0.1:8081/cgi/cgifir
st.exe?varget=testget"
 METHOD="POST">
<DIV>Введите что-нибудь:<BR>
<INPUT NAME="data" SIZE="60" MAXLENGTH="80"><BR>
<INPUT TYPE="SUBMIT" VALUE="Send"></DIV>
</FORM>
</body>
</html>

FORM ACTION=...  указываем скрипт, который будет обрабатывать наши данные
в этом запросе мы указываем еще и get запрос ?varget=testget,
метод, которым мы отрпавляем наши запросы значится в описании формы
<FORM ACTION="..." METHOD="POST">
Теперь, как только мы нажмем на кнопку "Send", данные уйдут на сервер.

На с++ код cgi может выглядеть так:
весь код http://www.cs.tut.fi/~jkorpela/forms/cgic.html
#include <stdio.h>
#include <stdlib.h>
#define MAXLEN 80
#define EXTRA 5
/* 4 for field name "data", 1 for "=" */
#define MAXINPUT MAXLEN+EXTRA+2
/* 1 for added line break, 1 for trailing NUL */
#define DATAFILE "../data/data.txt"

void unencode(char *src, char *last, char *dest)
{
 for(; src != last; src++, dest++)
   if(*src == '+')
     *dest = ' ';
   else if(*src == '%') {
     int code;
     if(sscanf(src+1, "%2x", &code) != 1) code = '?';
     *dest = code;
     src +=2; }    
   else
     *dest = *src;
 *dest = '\n';
 *++dest = '\0';
}

int main(void)
{
char *lenstr;
char input[MAXINPUT], data[MAXINPUT];
long len;
printf("%s%c%c\n",
"Content-Type:text/html;charset=iso-8859-1",13,10);
printf("<TITLE>Response</TITLE>\n");
lenstr = getenv("CONTENT_LENGTH");
if(lenstr == NULL || sscanf(lenstr,"%ld",&len)!=1 || len > MAXLEN)
  printf("<P>Error in invocation - wrong FORM probably.");
else {
  FILE *f;
  fgets(input, len+1, stdin);
  unencode(input+EXTRA, input+len, data);
  f = fopen(DATAFILE, "a");
  if(f == NULL)
    printf("<P>Sorry, cannot store your data.");
  else
    fputs(data, f);
  fclose(f);
  printf("<P>Thank you! Your contribution has been stored.");
  }
return 0;
}

lenstr = getenv("CONTENT_LENGTH"); // читаем длину
fgets(input, len+1, stdin); // читаем входной поток stdin
Функция unencode служит для преобразования символов, декодирования
Декодирование.
В CGI приложение данные приходят в специальном формате.
1. Имена полей и их значения разделяются знаком равно (=), name=value
2. Пары имя поля-значения разделяются амперсандом (&), name1=value1&name2=value2
3. Некондиционные символы (Inconvenient characters), такие как пробелы (заменяются +), амперсанды (%26), русские буквы заменяются hex эквивалентами
Например
    data=Русский текст // исходный
    data=%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9+%D1%82%D0%B5%D0%BA%D1%81%D1%82 // то, что получим в программе


Метод POST на delphi для windows
uses windows, sysutils;
...
 // Получение переданных параметров
if getvar('REQUEST_METHOD') = 'POST' then
begin
  post_vars := getvar('CONTENT_LENGTH'); // длина
  if post_vars <> '' then
  begin
    size := strtoint(post_vars);
    setlength(parmstring, size);
    for i := 1 to size do read(post_vars[i]); // читаем поток
  end;
end;

Продолжаем доработку того же проекта. Попытаемся получить POST запрос.
Используя стандартную библиотеку fcl (наше приложение по прежнему является наследником от TCustomCGIApplication
TCGIApp = class(TCustomCGIApplication))
Путем исследования исходного кода выясняем, что TCustomCGIApplication использует модуль httpdefs и его классы:
TCGIRequest - наследник httpdefs.TRequest, а он в свою очередь наследник httpdefs.THTTPHeader, в этом классе и есть
замечательные списки
    // Lists
    Property CookieFields : TStrings Read FCookieFields Write SetCookieFields; // здесь куки
    Property ContentFields: TStrings read FContentFields; // здесь хранятся данные POST запросов
    property QueryFields : TStrings read FQueryFields;  // здесь хранятся данные GET запросов

Таким образом, мы избавлены от небходимости самостоятельно читать поток STDIN, перекодировать переменные, вместо этого дописываем код в HandleRequest строки, отвечающие за получение данных POST запросов
   Resp_list.Add('<br>' + ' POST VARIABLE ');
   if ARequest.ContentFields.Count  <> 0 then
   begin
     for i := 0 to ARequest.ContentFields.Count -1 do
     Resp_list.Add(' <br> '+ inttostr(i) + ARequest.ContentFields[i]);
   end;

вся программа
program cgifirst;
{$mode objfpc}{$H+}
uses
  Classes, SysUtils, httpDefs, custcgi;
Type
{ TCGIApp } // класс наследник от  TCustomCGIApplication
TCGIApp = class(TCustomCGIApplication)
private

public
      Procedure HandleRequest(ARequest: Trequest; AResponse: TResponse);override;
end;

{ TCGIApp }
procedure TCGIApp.HandleRequest(ARequest: Trequest; AResponse: TResponse);
var
 Resp_list: TstringList;
 len, i: integer;
 stdIn, Size, Actual: cardinal;
 //Req_List: TstringList;
begin
  inherited HandleRequest(ARequest, AResponse);
  //AResponse.Content := inttostr(RequestVariableCount);
  Resp_list := TStringList.create;
  try
    // просто список с переменными  
   Resp_list.Add(' hello ');
   Resp_list.Add('<br>');
   Resp_list.Add(' You get variable is ' + EnvironmentVariable['QUERY_STRING']);
   Resp_list.Add(' method is ' + EnvironmentVariable['REQUEST_METHOD']);
   Resp_list.Add('<br>');
   Resp_list.Add(' SCRIPT_NAME ' + EnvironmentVariable['SCRIPT_NAME']);
   Resp_list.Add('<br>');
   Resp_list.Add(' PATH_INFO ' + EnvironmentVariable['PATH_INFO']);
    // длина контента
   Resp_list.Add(' length ' + inttostr(ARequest.ContentLength)+ LineEnding);
   // или так + EnvironmentVariable['CONTENT_LENGTH']);
   Resp_list.Add('<br>');
    // GET данные
   Resp_list.Add('<br>' + ' GET VARIABLE ');
   if ARequest.QueryFields.Count <> 0 then
   begin
     for i := 0 to ARequest.QueryFields.Count -1 do
     Resp_list.Add(' <br> '+ inttostr(i) + ' '+ ARequest.QueryFields[i]);
   end;
    // POST данные
   Resp_list.Add('<br>' + ' POST VARIABLE ');
   if ARequest.ContentFields.Count  <> 0 then
   begin
     for i := 0 to ARequest.ContentFields.Count -1 do
     Resp_list.Add(' <br> '+ inttostr(i) + ' '+ ARequest.ContentFields[i]);
   end;
   AResponse.Contents := Resp_list;
  finally
    Resp_list.free;
  end;
end;
// тело программы
begin
     with TCGIApp.create(nil) do
     try
        Initialize;
            // методы инициализации
            // FRequest:=TCGIRequest.CreateCGI(Self);
               // InitRequestVars; - инициализация переменных POST и GET                  
        Run;
     finally
        Free;
     end;
end.


Получаем
You get variable is varget=testget method is POST
SCRIPT_NAME /cgi/cgifirst.exe
PATH_INFO
length 12
GET VARIABLE
0 varget=testget
POST VARIABLE
0 data=russian

   
Почитать
http://httpd.apache.org/docs/2.2/howto/cgi.html настройка cgi в apache

http://w-wb.com/cgi/ - учебник по cgi
http://www.cs.tut.fi/~jkorpela/forms/cgic.html - cgi start (c++ пример)
http://php.su/learnphp/cgi
http://www.intuit.ru/department/internet/cgi/5/ - интуит

Delphi CGI
http://www.delphisources.ru/pages/faq/base/cgi_apps.html
http://mf.grsu.by/UchProc/konspekt/delphi_book/ch04/ch01
http://www.drbob42.com/books/cgi.htm

lazarus powtil (теперь объектно-ориентированный, известен также под именем PSP)
http://www.freepascal.ru/forum/viewtopic.php?f=6&t=3606 - простые примеры
http://powtils.googlecode.com/svn/dev/
http://sourceforge.net/projects/pascal-webdev/
http://z505.com/powtils/news.shtml


Пакет fcl-web (weblaz.lpk) Можно взять на source forge
установка
fcl-web, если он не установлен - топаем C:\lazarus\components\fpweb\weblaz.lpk
и устанавливаем этот пакет (запускаем Лазаря, открываем пакет, компилируем, даем команду установить и соглашаемся на пересборку Лазаря)
После установки этого пакета в меню создать ... мы можем лицезреть новый тип приложения - CGI Application, его и выбираем.
При этом создастся TFPWebModule с событиями:...


компоненты лазаря
http://wiki.lazarus.freepascal.org/Components_and_Code_examples

статья Леонардо CGI + ExtJs + Firebird на Лазаре !!! Особая благодарность

блог Леонардо leonardorame.blogspot