| ||
Hello, Perl! Perl FAQ по-русски. Часть 3Дмитрий Репин aka cmapuk[0nline] В этой части мы рассмотрим принципы работы с HTTP- и CGI-протоколами, а также начнём программировать CGI-приложения. HTTP, как он есть"GET мне вон тот mp3", - запросил клиент. Казалось бы, что начать рассказ о практическом применении Perl я должен был со столь популярного (особенно среди начинающих) CGI-программирования. Но для того, чтобы толком понять, как работает CGI, надо понять принципы взаимодействия клиента (броузера) и сервера, где лежат cgi-скрипты. Клиент и сервер - это, в простейшем варианте, консольные приложения, которые читают из стандартного ввода и пишут в стандартный вывод. Броузер, как программа для отображения страниц - всего лишь удобная красивая оболочка. Хотите убедиться? Пожалуйста! Наберите в консоли telnet. В Win32, если откроется белое окошко, в меню выберите "Подключить", введите адрес perl.ru, а в поле "Порт" поставьте 80. Если окошко не открылось, а открылась консоль вида telnet>, наберите open perl.ru 80. После подключения просто введите следующее: GET / HTTP/1.0 После этого вы получите на экран HTML-страницу с сервера. Вот и вся хитрость! Сервер и клиент обмениваются текстовой информацией по определенным правилам, совокупность которых и называется протоколом. В данном случае - HTTP. Запрос к серверу состоит из трех частей, в зависимости от метода запроса GET или POST. Есть и другие методы, но они редко используются и мы их рассматривать не будем. Запрос методом GET: Получить. GET /path/to/file.cgi?param1=value HTTP/1.0 Первая строка - 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 Метод POST отличается от GET следующими моментами: 1) Данные передаются не в первой строке с именем скрипта, а в третьей части, после всех переменных и пустой строки. 2) Переменная Content-length обязательна и должна содержать размер данных в байтах. 3) Поле Content-type содержит mime-тип посылаемых данных. Из этих строк и состоит HTTP-запрос. Все они пишутся программой-клиентом в стандартный вывод (STDOUT). Ниже мы более подробно рассмотрим запросы, уже создавая клиентские программы. Теперь остается разобраться: что же отвечает сервер на все эти запросы? Ответ сервера: 200 OK Found 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 тока быстро!", - опять запросил клиент. Нет, сейчас мы еще не будем писать скрипты. Сначала разберемся, что такое 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. # Как мы уже выяснили, данные клиента в скрипте мы принимаем либо из С приемом данных вроде разобрались. Стоит еще отметить, что все HTTP-заголовки, передаваемые клиентом в запросе, содержатся в хеше %ENV, а имена их содержат префикс HTTP_. Тире(дефисы) в этих заголовках в хеше %ENV превращаются в "_". Примеры: User-Agent $ENV{HTTP_USER_AGENT} Нельзя с уверенностью сказать, что данные в этих заголовках - чистая правда. Они передаются клиентом в запросе и могут быть запросто подделанными. Почему? Об этом мы поговорим ниже. А здесь надо заметить, что это не вся информация о киенте. Есть ведь еще REMOTE_ADDR, REMOTE_PORT, по которым можно идентифицировать клиента. Как я уже говорил, список всех заголовков вы найдете в RFC. Вернемся к программированию. # Теперь мы будем составлять ответ клиенту. В Перл-скриптах, по умолчанию, работает буферизация вывода. Это означает, что все print'ы сначала печатаются в буфер, и только в конце скрипта весь вывод выдается. За буферизацию отвечает встроенная переменная $|. Если $| определена (например $|=1), то буфер отключен. В этом случае вышеприведенный кусок кода выдаст ошибку 500, а в лог запишется сообщение о неправильном заголовке. Включение буферизации происходит так - undef $|;. Часто модули, для своей работы отключают буферизацию (например, при использовании баз данных), и это надо учитывать. В этом случае, весь наш вывод надо сохранить в переменной, которую и вывести в конце ОДНИМ print'ом. ... А зачем нужен заголовок Location? Для перенаправления клиента. Это похоже на перенаправление в . Пишется так: ... После этого уже никаких данных не надо, а все заголовки (и куки тоже) пишутся до этой строки. Что касается куков, то они записываются так: 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-скрипта. Выдать клиенту файл. ... Вот и все. Теперь рассмотрим обратную ситуацию - upload файлов. Первое правило: форма должна отправляться методом POST. Второе правило: тег <form> должен содержать параметр enctype="multipart/form-data". Допустим, наша форма содержит 2 тектовых поля и поле для файла: text1, coolfile, text2. # В скрипте будем читать данные из STDIN в двоичном режиме. Прочитали. И вот что мы имеем в переменной $buff: -----------------------------7d22c527e0250 При этом $ENV{CONTENT_TYPE} будет выглядеть так: multipart/form-data; boundary=---------------------------7d22c527e0250 Теперь каждый элемент разобьем на заголовок и данные и вытащим из заголовка имена полей. foreach $i(@blocks){ Можно еще учитывать тип данных (Content-type). Вот и все основные принципы работы с CGI. Немножко советовЧасто возникает необходимось идентифицировать посетителя на большом количестве страниц. Например, при входе в зону "для пользователей" требуется логин/пароль, и если пользователь должен производить какие-либо действия, его надо как-то распознавать, не сохраняя в HTML или в куках его приватных данных. Вот один из вариантов решения проблемы. Допустим, пользователь имеет идентификатор(ID) в системе, по которому в базе ведется работа с его данными, логин (login) и пароль (password). ID - это, в данном случае, скрытый внутренний параметр для общения базы и скриптов. Итак, пользователь заполнил форму входа в систему, т.е. ввел логин и пароль... Проверив существование пользователя, правильность пароля, мы должны сгенерировать уникальный идентификатор сессии SID. Метод генерации может быть каким угодно, главное, чтобы подбор SID представлял собой неразрешимую задачу. Например: srand($$^time); Хотя, в принципе, достаточно 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 |