ЧАСТЬ 3. В этой части мы рассмотрим принципы работы с 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 (www.comprice.ru)
|