IZONE- http://www.izcity.com/- бесплатный софт, вэб-сервисы, ресурсы для раскрутки, свежие номера журнала "Internet Zone".

Hello, Perl! Perl FAQ по-русски. Часть 3

Дмитрий Репин aka cmapuk[0nline]

В этой части мы рассмотрим принципы работы с HTTP- и CGI-протоколами, а также начнём программировать CGI-приложения.

HTTP, как он есть

"GET мне вон тот mp3", - запросил клиент.
"404 OK RTFM", - ответил сервер.
c2002 cmapuk[0nline]

Казалось бы, что начать рассказ о практическом применении Perl я должен был со столь популярного (особенно среди начинающих) CGI-программирования. Но для того, чтобы толком понять, как работает CGI, надо понять принципы взаимодействия клиента (броузера) и сервера, где лежат cgi-скрипты.

Клиент и сервер - это, в простейшем варианте, консольные приложения, которые читают из стандартного ввода и пишут в стандартный вывод. Броузер, как программа для отображения страниц - всего лишь удобная красивая оболочка. Хотите убедиться? Пожалуйста!

Наберите в консоли telnet. В Win32, если откроется белое окошко, в меню выберите "Подключить", введите адрес perl.ru, а в поле "Порт" поставьте 80. Если окошко не открылось, а открылась консоль вида telnet>, наберите open perl.ru 80. После подключения просто введите следующее:

GET / HTTP/1.0
User-Agent:Shmozilla 3000
<Нажмите 2 раза Enter>

После этого вы получите на экран HTML-страницу с сервера. Вот и вся хитрость! Сервер и клиент обмениваются текстовой информацией по определенным правилам, совокупность которых и называется протоколом. В данном случае - HTTP.

Запрос к серверу состоит из трех частей, в зависимости от метода запроса GET или POST. Есть и другие методы, но они редко используются и мы их рассматривать не будем.

Запрос методом GET: Получить.

GET /path/to/file.cgi?param1=value HTTP/1.0
User-Agent: Shmozilla 3000
Referer: http://www.necrosoft.com
Accept-Language:en;ru
<пустая строка >

Первая строка - 1-я часть запроса - делится на 3 части:

- GET - определяет метод.

- /path/to/file.cgi?param1=value1 - путь к файлу, который нам нужен, и параметры. Для строки в броузере http://www.perl.ru/go.cgi?action=forum это будет выглядеть как /go.cgi?action=forum.

- HTTP/1.0 - определяет версию протокола (или HTTP/1.1)

Далее идут переменные - 2-я часть запроса.

Эти переменные определяют название клиента, поддерживаемые языки, кодировку, и многое другое.

Полный список можно взять из спецификации по HTTP - RFC2616 (www.rfc.org). Кстати, все протоколы описаны в документах RFC, расшифровки номеров которых можно найти в документе, называемом rfcindex, проще говоря, в полном списке документов (опять же на www.rfc.org).

Третья часть - область данных - отделяется от второй пустой строкой.

В методе GET эта часть - пустая. То есть, признаком конца запроса будет последовательность из двух переводов строки - "\n\n".

Запрос методом POST: Послать.

POST /path/to/file.cgi HTTP/1.0
User-Agent: Shmozilla 3000
Referer: http://www.necrosoft.com
Content-length:42
Content-type:application/www-form-urlencoded
<пустая строка >
param1=value1¶m2=value2¶m3-=value3

Метод POST отличается от GET следующими моментами:

1) Данные передаются не в первой строке с именем скрипта, а в третьей части, после всех переменных и пустой строки.

2) Переменная Content-length обязательна и должна содержать размер данных в байтах.

3) Поле Content-type содержит mime-тип посылаемых данных.

Из этих строк и состоит HTTP-запрос. Все они пишутся программой-клиентом в стандартный вывод (STDOUT). Ниже мы более подробно рассмотрим запросы, уже создавая клиентские программы.

Теперь остается разобраться: что же отвечает сервер на все эти запросы?

Ответ сервера:

200 OK Found
Content-Length:1024
...
Content-type:text/plain
<пустая строка>

1024 байт данных

Ответ сервера тоже состоит из трех частей:

- Первая строка - 1 часть.

- ХХХ - цифровой код ошибки.

- OK, Error, etc. - Код словесный =)

- Found, Not Found, etc. - расшифровка ответа.

- Вторая часть - опять же переменные, говорящие о многом =).

- Третья часть - после пустой строки - данные, размер которых обозначен в переменной Content-Length.

Некоторые коды:

- 2ХХ - различные ОК'ейные ответы.

- 4ХХ - Ошибки категории File not found, Authorization error, и прочие.

- 5ХХ - Ошибки сервера (проклятая 500 Error из этой категории).

Расшифровку всех кодов, а также переменных можно увидеть все в том же RFC2616.

Теперь можно перейти и к CGI.

Клиент -> Сервер -> Скрипт, и обратно

"GET /mp3filez/cool.mp3 тока быстро!", - опять запросил клиент.
"404 RTFM, я ведь сказал", - ответил сервер.
c2002 cmapuk[0nline]

Нет, сейчас мы еще не будем писать скрипты. Сначала разберемся, что такое CGI.

Итак, связь между клиентом и нашим скриптом происходит через посредничество самого сервера. Клиент с сервером общаются, как мы уже выяснили, по протоколу HTTP, а протокол CGI нужен для связи между сервером и скриптом. В предыдущей главе я намеренно назвал HTTP-заголовки(Content-type, etc.) переменными для того, чтобы проследить связи между этими заголовками и Perl-хешем переменных окружения %ENV. Разберемся.

Сервер получает запрос и раскладывает его на составные части. По первой строке он определяет, какой скрипт запускать, метод запроса, версию протокола и заголовки. После этого он знает:

1) Откуда брать данные: из первой строки после имени скрипта и "?" или из 3-й части запроса.

2) Куда эти данные положить скрипту - в $ENV{QUERY_STRING} или в STDIN.

Все заголовки, метод запроса и версию протокола сервер забивает в окружение, которое доступно скрипту из %ENV. Вот примерно таким образом мы и получаем в скрипте клиентские данные. Яснее разжевать не могу ;-).

Теперь о том, как скрипт отдает данные серверу для клиента. Сервер разрешает скрипту самому ставить заголовки, которые он потом отправит клиенту. Виды и форматы заголовков все те же, что в HTTP-спецификации. Пишется это все скриптом в стандартный вывод. Самое главное правило - в выводе должен быть заголовок "Content-type" или "Location" и ОН ДОЛЖЕН БЫТЬ ПОСЛЕДНИМ!!!. После последнего заголовка пишется пустая строка (то есть "\n\n"), а затем, если это не Location, данные, которые должен получить клиент. Вот и весь принцип работы. Для того чтобы заниматься CGI-программированием, эти простые истины НУЖНО ЗНАТЬ!!! А теперь будем практиковаться )).

CGI, или сетевая Камасутра

В русском издании "Perl CookBook" глава "Программирование CGI" начинается со страницы ї666. Что бы это значило?
Просто наблюдение.

CGI-программирование - основная область работы для Перл-программистов, особенно начинающих. По этой причине объем вопросов в форумах по этой теме составляет, вероятно, процентов 45 (Еще 45 на базы данных, а оставшиеся 10 - вопросы настройки Apache ;-)). Поэтому можно смело сказать, что CGI - это главный враг начинающего программиста. Шутка =).

По причинам личной неприязни, модуль CGI.pm я тут описывать не буду. Если понять принцип работы с протоколом CGI, то для использования этого модуля достаточно будет стандартной документации. Напротив, не зная, что из себя представляет протокол, программирование с CGI.pm будет неотрывно связано с ошибками, глупыми вопросами и т.п. Безусловно, модуль CGI.pm решает очень много программных задач, но если в скрипте все эти решения не нужны, зачем заставлять интерпретатор обрабатывать более 214 килобайт кода?!!! Итак, CGI без CGI.pm.

# Как мы уже выяснили, данные клиента в скрипте мы принимаем либо из
# $ENV{QUERY_STRING}, либо из STDIN. Выглядит это так.
$ENV{REQUEST_METHOD} eq "GET"?$data=$ENV{QUERY_STRING}:-read(STDI-N,$data,$ENV{CONTENT_LENGTH});
# Здесь мы использовали "хитрый" условный оператор "?:"
# Принцип: (Выражение-условие)?(ВыражениеЕслиTrue):-(ВыражениеЕслиFalse);
# Таким образом, если данные передаются как GET, переменная $data
# заполняется из QUERY_STRING, то есть из строки параметров запроса.
# Если же метод - POST, мы берем данные из стандартного ввода
# с помощью функции read (perldoc -f read). Эта функция требует
# в качестве третьего параметра - количество байт для чтения. Это
# мы узнаем из CONTENT_LENGTH.
# Теперь в $data лежит что-то типа param1=value1¶m2=value2
@pairs=split/\&/,$data;
# Разбили на пары param=value
# Теперь раскладываем наши пары на ключи и значения и кладем в хэш
foreach $pair (@pairs){
($key,$val)=split/=/,$pair;
$val=~s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",hex($1))/eg;
# Это для того, чтобы превратить белиберду типа %2C в нормальные
# знаки. В частности, русские буквы.
$val=~s/\+/ /g;
# Пробелы передаются как "+", мы их возвращаем в нормальный вид
         $F{$key}=~s/\r//g;
         # В многострочном текстовом поле формы переводы строк могут
         # выглядеть как \r\n, мы эти \r убиваем, так как оно нам не надо.
         $F{$key}=$val;
         # Создаем хэш ключ=>значение
}
# Теперь все параметры формы у нас в хэше.
# Если в форме присутствуют параметры с одинаковыми именами,
# например чекбоксы, то строку $F{$key}=$val можно заменить на
if(!$F{$key}){
         $F{$key}=$val;
}else{
         $F{$key}.="\n$val";
}
# таким образом одноименные параметры мы превратили в многострочную
# строку, которую потом можно разбить в массив.

С приемом данных вроде разобрались. Стоит еще отметить, что все HTTP-заголовки, передаваемые клиентом в запросе, содержатся в хеше %ENV, а имена их содержат префикс HTTP_. Тире(дефисы) в этих заголовках в хеше %ENV превращаются в "_". Примеры:

User-Agent       $ENV{HTTP_USER_AGENT}
Cookie           $ENV{HTTP_COOKIE}
Referer          $ENV{HTTP_REFERER}

Нельзя с уверенностью сказать, что данные в этих заголовках - чистая правда. Они передаются клиентом в запросе и могут быть запросто подделанными. Почему? Об этом мы поговорим ниже. А здесь надо заметить, что это не вся информация о киенте. Есть ведь еще REMOTE_ADDR, REMOTE_PORT, по которым можно идентифицировать клиента. Как я уже говорил, список всех заголовков вы найдете в RFC.

Вернемся к программированию.

# Теперь мы будем составлять ответ клиенту.
# После получения данных, мы их обработали и уже решили,
# какие данные нам вернуть.
...
print "Cool-header:blablabla\n";
print "Content-Charset:gluckowin-1251\n";
print "Content-type:text/html\n\n";
print "<html>Basile Pupkin was here!</html>";
# Сначала мы выводим нужные заголовки.
# Это могут быть описания данных(типа Content-Charset), куки и пр.
# ПОСЛЕДНИМ заголовком должен быть либо Content-type, либо Location
# После этого заголовка - \n\n, а дальше данные, если надо.
# Здесь следует отметить важный момент.

В Перл-скриптах, по умолчанию, работает буферизация вывода. Это означает, что все print'ы сначала печатаются в буфер, и только в конце скрипта весь вывод выдается. За буферизацию отвечает встроенная переменная $|. Если $| определена (например $|=1), то буфер отключен. В этом случае вышеприведенный кусок кода выдаст ошибку 500, а в лог запишется сообщение о неправильном заголовке. Включение буферизации происходит так - undef $|;. Часто модули, для своей работы отключают буферизацию (например, при использовании баз данных), и это надо учитывать. В этом случае, весь наш вывод надо сохранить в переменной, которую и вывести в конце ОДНИМ print'ом.

...
$OUT="Cool-header:blablabla\n";
$OUT.="Content-Charset:gluckowin-1251\n";
$OUT.="Content-type:text/html\n\n";
$OUT.="<html>Basile Pupkin was here!</html>";
...
print $OUT;

# или так:
...
$OUT=< Cool-header:blablabla
Content-Charset:gluckowin-1251
Content-type:text/html
<html>Basile Pupkin was here!<br>
Yo!
...
</html>
OUT_DATA
print $OUT;
#

А зачем нужен заголовок Location? Для перенаправления клиента. Это похоже на перенаправление в . Пишется так:

...
print "Location:http://www.perl.com\n\n";

После этого уже никаких данных не надо, а все заголовки (и куки тоже) пишутся до этой строки. Что касается куков, то они записываются так:

print "Set-Cookie:$COOKIE_DATA\n";

А какой формат имеет $COOKIE_DATA, можно и НУЖНО посмотреть в соответствующей спецификации.

Куки придумали в Netscape.Вот здесь можно про них почитать (кроме www.rfc.org конечно) http://developer.netscape.com/ docs/manuals/js/client/ jsref/cookies.htm

Пример я все же приведу:

Set-Cookie: user=admin ; Expires=Thursday, 12-Nov-02 19:19:19 GMT;

Вот простенькая кука.

В таком же виде куки принимаются из $ENV{HTTP_COOKIE}.

Еще одна, часто встречающаяся задача для cgi-скрипта. Выдать клиенту файл.

...
$bytes = -s "coolfile.zip"; # Определяем размер файла (perldoc perlop)
open(F,"coolfile.zip");
binmode F;                       # Устанавливаем двоичный режим чтения
read(F,$zip,$bytes);       # Читаем весь файл в переменную $zip
close(F);
print "Content-length: $bytes\n"; # Выводим заголовки
print "Content-Disposition: attachment; filename=coolfile.zip\n";
print "Content-type: application/octet-stream\n\n";
binmode STDOUT;             # На вывод тоже двоичный режим
print $zip;                 # Выводим сам файл
...

Вот и все. Теперь рассмотрим обратную ситуацию - upload файлов.

Первое правило: форма должна отправляться методом POST.

Второе правило: тег <form> должен содержать параметр enctype="multipart/form-data".

Допустим, наша форма содержит 2 тектовых поля и поле для файла: text1, coolfile, text2.

# В скрипте будем читать данные из STDIN в двоичном режиме.
binmode STDIN;
read(STDIN,$buff,$ENV{CONTENT_LENGTH});

Прочитали. И вот что мы имеем в переменной $buff:

-----------------------------7d22c527e0250
Content-Disposition: form-data; name="text1"

Это текст1 тралала
-----------------------------7d22c527e0250
Content-Disposition: form-data; name="cool"; filename="X:\file.gif"
Content-Type: image/gif

GIF87 Здесь двоичные данные файла
-----------------------------7d22c527e0250
Content-Disposition: form-data; name="text2"

Это текст2 трулала
-----------------------------7d22c527e0250--


При этом $ENV{CONTENT_TYPE} будет выглядеть так:

multipart/form-data; boundary=---------------------------7d22c527e0250
Теперь из CONTENT_TYPE нужно взять параметр boundary и по нему разбить нашу переменную $buff в массив.
($boundary = $ENV{CONTENT_TYPE}) =~ s/^.*boundary=(.*)$/\1/;
@blocks = split/--$boundary/, $buf;
@blocks = splice(@blocks,1,$#blocks-1);
# Крайние элементы окажутся пустыми - обрезаем.
Теперь массив @blocks содержит 3 блока данных
$blocks[0]
Content-Disposition: form-data; name="text1"

<Это текст1 тралала>
$blocks[1]
Content-Disposition: form-data; name="cool"; filename="X:\file.gif"
Content-Type: image/gif

<Здесь двоичные данные файла>
$blocks[3]
Content-Disposition: form-data; name="text2"

<Это текст2 трулала>

Теперь каждый элемент разобьем на заголовок и данные и вытащим из заголовка имена полей.

foreach $i(@blocks){
         ($head,$data)=split/\n\n/,$i;
         $head=~/ name="(\w+)"/;
         $name=$1;
         $F{$name}=$data;
}
# А теперь записываем файл
open(F,">$uploadedfile");
binmode(F);
print F $F{file};
close(F);


Можно еще учитывать тип данных (Content-type).

Вот и все основные принципы работы с CGI.

Немножко советов

Часто возникает необходимось идентифицировать посетителя на большом количестве страниц. Например, при входе в зону "для пользователей" требуется логин/пароль, и если пользователь должен производить какие-либо действия, его надо как-то распознавать, не сохраняя в HTML или в куках его приватных данных. Вот один из вариантов решения проблемы.

Допустим, пользователь имеет идентификатор(ID) в системе, по которому в базе ведется работа с его данными, логин (login) и пароль (password). ID - это, в данном случае, скрытый внутренний параметр для общения базы и скриптов. Итак, пользователь заполнил форму входа в систему, т.е. ввел логин и пароль...

Проверив существование пользователя, правильность пароля, мы должны сгенерировать уникальный идентификатор сессии SID. Метод генерации может быть каким угодно, главное, чтобы подбор SID представлял собой неразрешимую задачу. Например:

srand($$^time);
$ipx=$ENV{REMOTE_ADDR};
$timex=rand time;
$SID.=crypt($ipx,$timex) x 5;
# способ жуткий =)

Хотя, в принципе, достаточно 10-значного числового SID. Здесь каждый действует, руководствуясь своей паранойей. Допустим, мы его все-таки сгенерили. Теперь мы записываем либо в БД, либо просто в текстовый файл следующие 3 параметра: SID, TIME, ID. Здесь SID - это то чудо, которое мы только что сгенерили, TIME - это время в секундах с начала эпохи, полученное функцией time(), а ID - это идентификатор пользователя в системе. Теперь нам нужно снабдить пользователя нашим SID'ом. Это можно сделать двумя способами:

1) Записать SID ему в куки.

2) Использовать SID во всех ссылках и формах внутри пользовательской зоны.

К чему все это? К тому, что после входа в закрытую зону пользователь при любой операции будет отправлять нам SID. А мы будем:

1) Проверять, есть ли такой SID (если нет - отправляем на страницу входа).

2) Проверять параметр TIME и сравнивать его с полученным снова числом из time().

Если TIME меньше значения функции time() на 300 и более сек., значит, данный пользователь уже 5 минут нас не юзал (видимо, он ушел на обед, а злой сослуживец решил ему напакостить ;-)), - отправляем на вход.

Если же такой SID существует и "время не вышло", то мы перезаписываем в БД или файл параметр TIME на текущий и используем для обработки данных тот ID, который соответствует SID в записях. И все повторяется снова. Таким образом, мы сохраняем безопасность пользователя. Его пароль не останется ни в кэше браузера, ни в злосчастных куках. Реализацию способа, при котором все линки снабжаются SID'ом, можно посмотреть на примере чата www.divan.ru. Зайдите в чат и посмотрите HTML-ресурсы его страниц...

Вообще, методов много, включая "не-cgi-скриптовые" типа htaccess и т.п. А это был всего лишь маленький пример для раззадоривания фантазии )).

Опасная профессия

Я не националист и не коммунист, но глубокая уверенность в криворукости рук и прямоизвилинности мозга "буржуинских" программеров во мне живет и крепчает благодаря постоянным подтверждениям моей позиции. CGI-скрипты - это широкие ворота для допуска клиента к информации на сервере. И задача программиста на 50% заключается в том, чтобы подобрать надежную охрану для этих ворот (которые, впрочем, он сам и открывает). Проверка параметров, передаваемых в функцию open(), для меня лично стала чуть ли не автоматически выполняемой задачей при программировании. Я уже не говорю о таких опасностях, как system() и т.п. По сути дела, open() - это почти шелл (командная строка), и обращаться с ней надо с соответствующей осторожностью. Итак, совет первый: при написании программы использовать в первой строке скрипта ключ -T. Этот параметр включает так назваемый "зараженный" режим. В этом режиме, при попытке передать непроверенные данные, полученные извне, в опасную часть кода - Перл выдаст соответствующее сообщение. Это заставит вас "обеззаразить" все переменные. Подробнее об этом (и не только) ключе - perldoc perlrun.

Если вы все же не хотите мучиться с "зараженными" переменными и работать с файлами, то...

1. Если ваш скрипт выдает клиенту файлы (как в примере про coolfile.zip), то НИ В КОЕМ СЛУЧАЕ НЕЛЬЗЯ делать так: script.cgi?file=/zips/coolfile.zip !!! Не уподобляйтесь Василию Пупкину и создайте таблицу соответствий (БД/Хэш/файл), в которой будут пары типа: 0001=/zips/coolfile.zip, и принимайте в качестве параметра скрипта 0001. Кстати, скрипт go.cgi на perl.ru принимаемое значение параметра board вставляет без проверки в open(). Выглядит это примерно так:

open(F,"/path/to/dir/$PARAM.dat");

БАРДАК!!!

Даже если и вставлять параметр таким образом, то достаточно простой проверки:

print_error("Go home,LLamaz!") if $PARAM=~/[^a-zA-Z]+/;

Лучше все-таки быть параноиком =).

Файлы - не единственная опасность. Допустим, новости на нашем сайте берутся из базы. Вызов скрипта, который все это показывает, выглядит так: script.cgi?display=news. Невероятно часто в скриптах встречается такая ошибка: строка news является именем таблицы (или колонки таблицы) БД и НЕ ПРОВЕРЯЕТСЯ. И представьте, что теперь можно сделать с этой базой! script.cgi?display=тут_любые_sql_команды. Вот такая история.

БУДЬТЕ БДИТЕЛЬНЫ!

В следующей части этого опуса мы будем учиться программировать клиент-серверные приложения с использованием модулей Socket, IO::Socket и LWP, а также рассмотрим вопросы работы с базами данных посредством Perl.

Источник: "Компьютер Price", http://www.comprice.ru

 


Copyright © "Internet Zone", http://www.izcity.com/, info@izcity.com