Содержание страницы
20.2. cgi – поддержка Common Gateway Interface¶cgi – Common Gateway Interface support
Исходный код: Lib/cgi.py
Вспомогательный модуль для скриптов Common Gateway Interface (CGI).
Этот модуль определяет ряд утилит для использования CGI-скриптами, написанными на Python.
20.2.1. Введение¶Introduction
CGI-скрипт вызывается HTTP-сервером, обычно для обработки пользовательского ввода, отправленного через элемент HTML <FORM> или <ISINDEX>.
Чаще всего CGI-скрипты находятся в специальной директории сервера cgi-bin. HTTP-сервер помещает всю информацию о запросе (такую как имя хоста клиента, запрошенный URL, строку запроса и множество других полезных данных) в окружение оболочки скрипта, выполняет скрипт и отправляет его вывод обратно клиенту.
Ввод скрипта также подключён к клиенту, и иногда данные формы читаются таким образом; в других случаях данные формы передаются через часть URL «строка запроса». Этот модуль предназначен для обработки различных ситуаций и предоставления более простого интерфейса для Python-скрипта. Он также предоставляет ряд утилит, помогающих при отладке скриптов, и последним дополнением является поддержка загрузки файлов из формы (если браузер это поддерживает).
Вывод CGI-скрипта должен состоять из двух разделов, разделённых пустой строкой. Первый раздел содержит несколько заголовков, сообщающих клиенту, какие данные следуют. Код Python для генерации минимального раздела заголовков выглядит так:
print("Content-Type: text/html") # Далее HTML
print() # пустая строка, конец заголовков
Второй раздел обычно представляет собой HTML, что позволяет клиентскому программному обеспечению отображать красиво отформатированный текст с заголовками, встроенными изображениями и т.д. Вот код Python, выводящий простой фрагмент HTML:
print("<TITLE>CGI script output</TITLE>")
print("<H1>This is my first CGI script</H1>")
print("Hello, world!")
20.2.2. Использование модуля cgi¶Using the cgi module
Для начала следует написать import cgi.
При написании нового скрипта стоит добавить следующие строки:
import cgitb
cgitb.enable()
Это активирует специальный обработчик исключений, который будет выводить подробные отчеты в веб-браузере при возникновении ошибок. Если вы не хотите показывать пользователям вашего скрипта внутренности программы, можно вместо этого сохранять отчеты в файлы с помощью такого кода:
import cgitb
cgitb.enable(display=0, logdir="/path/to/logdir")
Очень полезно использовать эту возможность при разработке скрипта. Отчёты, формируемые cgitb, содержат информацию, способную сэкономить много времени при поиске ошибок. Строку cgitb можно удалить позже, после тестирования скрипта и уверенности в его корректной работе.
Чтобы получить отправленные данные формы, используйте класс FieldStorage. Если форма содержит не-ASCII символы, используйте именованный параметр encoding, установленный в значение кодировки, определённой для документа. Обычно она содержится в теге META в разделе HEAD HTML-документа или в заголовке Content-Type). Это считывает содержимое формы из стандартного ввода или окружения (в зависимости от значений различных переменных окружения, установленных согласно стандарту CGI). Поскольку это может потребить стандартный ввод, создавать экземпляр следует только один раз.
Экземпляр FieldStorage можно индексировать как словарь Python. Он поддерживает проверку принадлежности с помощью оператора in, а также стандартный метод словаря keys() и встроенную функцию len(). Поля формы, содержащие пустые строки, игнорируются и не появляются в словаре; чтобы сохранить такие значения, передайте истинное значение для необязательного именованного параметра keep_blank_values при создании экземпляра FieldStorage.
Например, следующий код (который предполагает, что заголовок Content-Type и пустая строка уже выведены) проверяет, что поля name и addr установлены в непустую строку:
form = cgi.FieldStorage()
if "name" not in form or "addr" not in form:
print("<H1>Error</H1>")
print("Please fill in the name and addr fields.")
return
print("<p>name:", form["name"].value)
print("<p>addr:", form["addr"].value)
...further form processing here...
Здесь поля, доступ к которым осуществляется через form[key], сами являются экземплярами FieldStorage (или MiniFieldStorage, в зависимости от кодировки формы). Атрибут value экземпляра возвращает строковое значение поля. Метод getvalue() возвращает это строковое значение напрямую; он также принимает необязательный второй аргумент – значение по умолчанию, которое возвращается, если запрошенный ключ отсутствует.
Если отправленные данные формы содержат несколько полей с одинаковым именем, то объект, полученный через form[key], будет не экземпляром FieldStorage или MiniFieldStorage, а списком таких экземпляров. В этой ситуации form.getvalue(key) также вернёт список строк. Если вы ожидаете такую возможность (когда ваша HTML-форма содержит несколько полей с одинаковым именем), используйте функцию getlist(), которая всегда возвращает список значений (чтобы не приходилось обрабатывать случай с одним элементом отдельно). Например, следующий код объединяет любое количество полей username, разделяя их запятыми:
value = form.getlist("username")
usernames = ",".join(value)
Если поле представляет загруженный файл, доступ к значению через атрибут value или метод getvalue() считывает весь файл в память как bytes. Возможно, это не то, что вам нужно. Можно проверить, является ли поле загруженным файлом, проверив атрибут filename или атрибут file. Затем можно не спеша читать данные из атрибута file (методы read() и readline() будут возвращать bytes):
fileitem = form["userfile"]
if fileitem.file:
# Это загруженный файл; подсчитать строки
linecount = 0
while True:
line = fileitem.file.readline()
if not line: break
linecount = linecount + 1
Если при получении содержимого загруженного файла возникает ошибка (например, когда пользователь прерывает отправку формы, нажав кнопку «Назад» или «Отмена»), атрибут done объекта поля будет установлен в значение -1.
Проект стандарта загрузки файлов предусматривает возможность загрузки нескольких файлов из одного поля (с использованием рекурсивной кодировки multipart/*). В этом случае элемент будет похожим на словарь FieldStorage. Это можно определить, проверив его атрибут type, который должен быть multipart/form-data (или, возможно, другой MIME-тип, соответствующий multipart/*). В этом случае по нему можно рекурсивно итерироваться, как и по объекту формы верхнего уровня.
Когда форма отправляется в «старом» формате (как строка запроса или как одна часть данных типа application/x-www-form-urlencoded), элементы будут экземплярами класса MiniFieldStorage. В этом случае атрибуты list, file и filename всегда равны None.
Форма, отправленная через POST с дополнительной строкой запроса, будет содержать элементы как FieldStorage, так и MiniFieldStorage.
20.2.3. Интерфейс верхнего уровня¶Higher Level Interface
В предыдущем разделе объясняется, как читать данные CGI-формы с помощью класса FieldStorage. В этом разделе описывается интерфейс более высокого уровня, добавленный в этот класс для обеспечения более читаемого и интуитивного способа работы. Этот интерфейс не делает устаревшими методы, описанные в предыдущих разделах – они по-прежнему полезны, например, для эффективной обработки загрузки файлов.
Интерфейс состоит из двух простых методов. С их помощью можно обрабатывать данные формы универсально, не задумываясь о том, было ли отправлено одно или несколько значений под одним именем.
В предыдущем разделе было показано, как писать следующий код каждый раз, когда ожидается, что пользователь отправит несколько значений под одним именем:
item = form.getvalue("item")
if isinstance(item, list):
# Пользователь запрашивает более одного элемента.
else:
# Пользователь запрашивает только один элемент.
Такая ситуация часто возникает, например, когда форма содержит группу флажков с одинаковым именем:
<input type="checkbox" name="item" value="1" />
<input type="checkbox" name="item" value="2" />
Однако в большинстве случаев в форме есть только один элемент управления с определённым именем, и тогда ожидается и требуется только одно значение, связанное с этим именем. Поэтому пишется скрипт, содержащий, например, такой код:
user = form.getvalue("user").upper()
Проблема с кодом в том, что никогда не следует ожидать, что клиент предоставит корректные входные данные для скриптов. Например, если любопытный пользователь добавит ещё одну пару user=foo в строку запроса, скрипт аварийно завершится, поскольку в этой ситуации вызов метода getvalue("user") возвращает список вместо строки. Вызов метода upper() для списка недопустим (так как в списках нет такого метода) и приводит к исключению AttributeError.
Поэтому единственным правильным способом чтения значений данных формы было всегда использовать код, проверяющий, является ли полученное значение одиночным или списком значений. Это утомительно и снижает читаемость скриптов.
Более удобный подход – использовать методы getfirst() и getlist(), предоставляемые этим интерфейсом верхнего уровня.
- FieldStorage.getfirst(name, default=None)¶
Этот метод всегда возвращает только одно значение, связанное с полем формы name. Метод возвращает только первое значение в случае, если под таким именем было отправлено несколько значений. Обратите внимание: порядок получения значений может различаться в разных браузерах, и на него не следует полагаться. [1] Если поле формы или значение отсутствует, метод возвращает значение, указанное необязательным параметром default. Если параметр не указан, по умолчанию используется None.
- FieldStorage.getlist(name)¶
Этот метод всегда возвращает список значений, связанных с полем формы name. Метод возвращает пустой список, если для name не существует такого поля или значения. Возвращает список из одного элемента, если существует только одно значение.
С помощью этих методов можно писать компактный код:
import cgi
form = cgi.FieldStorage()
user = form.getfirst("user", "").upper() # Так это безопасно.
for item in form.getlist("item"):
do_something(item)
20.2.4. Функции¶Functions
Они полезны, если нужен более тонкий контроль или если требуется использовать некоторые алгоритмы, реализованные в этом модуле, в других ситуациях.
- cgi.parse(fp=None, environ=os.environ, keep_blank_values=False, strict_parsing=False)¶
Разбирает запрос из окружения или из файла (по умолчанию файл – sys.stdin). Параметры keep_blank_values и strict_parsing передаются в urllib.parse.parse_qs() без изменений.
- cgi.parse_qs(qs, keep_blank_values=False, strict_parsing=False)¶
Эта функция устарела в данном модуле. Вместо неё используйте urllib.parse.parse_qs(). Она поддерживается здесь только для обратной совместимости.
- cgi.parse_qsl(qs, keep_blank_values=False, strict_parsing=False)¶
Эта функция устарела в данном модуле. Вместо неё используйте urllib.parse.parse_qs(). Она поддерживается здесь только для обратной совместимости.
- cgi.parse_multipart(fp, pdict)¶
Разбирает входные данные типа multipart/form-data (для загрузки файлов). Аргументы: fp – входной файл, pdict – словарь, содержащий другие параметры из заголовка Content-Type.
Возвращает словарь, аналогичный urllib.parse.parse_qs(): ключи – имена полей, каждое значение – список значений для этого поля. Это легко использовать, но не очень хорошо, если ожидается загрузка мегабайтов – в этом случае используйте класс FieldStorage, который гораздо более гибок.
Обратите внимание, что эта функция не разбирает вложенные части multipart – для этого используйте FieldStorage.
- cgi.parse_header(string)¶
Разбирает MIME-заголовок (например, Content-Type) на основное значение и словарь параметров.
- cgi.test()¶
Надёжный тестовый CGI-скрипт, который можно использовать как основную программу. Выводит минимальные HTTP-заголовки и форматирует всю переданную скрипту информацию в виде HTML.
- cgi.print_environ()¶
Форматирует окружение оболочки в формате HTML.
- cgi.print_form(form)¶
Форматирование формы в HTML.
- cgi.print_directory()¶
Форматирование текущей директории в HTML.
- cgi.print_environ_usage()¶
Вывод в HTML списка полезных переменных окружения (используемых CGI).
- cgi.escape(s, quote=False)¶
Преобразует символы '&', '<' и '>' в строке s в HTML-безопасные последовательности. Используйте это, если нужно отобразить текст, который может содержать такие символы в HTML. Если необязательный флаг quote равен true, символ кавычки (") также преобразуется; это помогает при встраивании в значение атрибута HTML, ограниченное двойными кавычками, как в <a href="...">. Обратите внимание: одинарные кавычки никогда не преобразуются.
Устарело с версии 3.2: Эта функция небезопасна, поскольку по умолчанию quote равен false, и поэтому она устарела. Вместо неё используйте html.escape().
20.2.5. Вопросы безопасности¶Caring about security
Есть одно важное правило: если вы вызываете внешнюю программу (через функции os.system() или os.popen() или другие с аналогичной функциональностью), убедитесь, что вы не передаёте оболочке произвольные строки, полученные от клиента. Это хорошо известная уязвимость, с помощью которой опытные хакеры из любой точки сети могут использовать доверчивый CGI-скрипт для выполнения произвольных команд оболочки. Даже части URL или имена полей не заслуживают доверия, поскольку запрос не обязательно должен приходить из вашей формы!
Чтобы быть в безопасности, если вам всё же нужно передать строку из формы команде оболочки, стоит убедиться, что строка содержит только буквенно-цифровые символы, дефисы, символы подчёркивания и точки.
20.2.6. Установка CGI-скрипта в системе Unix¶Installing your CGI script on a Unix system
Прочитайте документацию вашего HTTP-сервера и уточните у системного администратора каталог, в который следует устанавливать CGI-скрипты; обычно это каталог cgi-bin в дереве сервера.
Убедитесь, что ваш скрипт доступен для чтения и выполнения «другими»; в Unix режим файла должен быть 0o755 восьмеричный (используйте chmod 0755 filename). Убедитесь, что первая строка скрипта содержит #!, начиная с первой колонки, после которого идёт путь к интерпретатору Python, например:
#!/usr/local/bin/python
Убедитесь, что интерпретатор Python существует и доступен для выполнения «другими».
Убедитесь, что любые файлы, которые ваш скрипт должен читать или записывать, доступны для чтения или записи соответственно «другим» – их режим должен быть 0o644 для чтения и 0o666 для записи. Это связано с тем, что из соображений безопасности HTTP-сервер выполняет ваш скрипт от имени пользователя «nobody», без каких-либо особых привилегий. Он может только читать (записывать, выполнять) файлы, которые все могут читать (записывать, выполнять). Текущий каталог во время выполнения также отличается (обычно это каталог cgi-bin сервера), и набор переменных окружения тоже отличается от того, что вы получаете при входе в систему. В частности, не рассчитывайте, что путь поиска исполняемых файлов оболочки (PATH) или путь поиска модулей Python (PYTHONPATH) будут установлены во что-то интересное.
Если вам нужно загружать модули из директории, которой нет в пути поиска модулей Python по умолчанию, можно изменить путь в скрипте перед импортом других модулей. Например:
import sys
sys.path.insert(0, "/usr/home/joe/lib/python")
sys.path.insert(0, "/usr/local/lib/python")
(Так директория, вставленная последней, будет просматриваться первой!)
Инструкции для систем, отличных от Unix, различаются; ознакомьтесь с документацией вашего HTTP-сервера (обычно в ней есть раздел о CGI-скриптах).
20.2.7. Тестирование CGI-скрипта¶Testing your CGI script
К сожалению, CGI-скрипт обычно не работает при запуске из командной строки, а скрипт, отлично работающий в командной строке, может таинственно отказываться при запуске через сервер. Но есть одна причина, по которой всё же стоит тестировать скрипт из командной строки: если в нём есть синтаксическая ошибка, интерпретатор Python вообще не выполнит его, и HTTP-сервер, скорее всего, отправит клиенту малопонятную ошибку.
Если в скрипте нет синтаксических ошибок, но он всё равно не работает, ничего не остаётся, кроме как прочитать следующий раздел.
20.2.8. Отладка CGI-скриптов¶Debugging CGI scripts
Прежде всего проверьте тривиальные ошибки установки – внимательное прочтение раздела выше об установке CGI-скрипта может сэкономить много времени. Если вы сомневаетесь, правильно ли поняли процедуру установки, попробуйте установить копию этого файла модуля (cgi.py) как CGI-скрипт. При вызове как скрипт файл выведет своё окружение и содержимое формы в виде HTML. Задайте ему правильный режим и т.д. и отправьте запрос. Если он установлен в стандартный каталог cgi-bin, то можно отправить запрос, введя в браузере URL следующего вида:
http://yourhostname/cgi-bin/cgi.py?name=Joe+Blow&addr=At+Home
Если это даёт ошибку 404, сервер не может найти скрипт – возможно, его нужно установить в другой каталог. Если возникает другая ошибка, значит, есть проблема с установкой, которую следует устранить, прежде чем двигаться дальше. Если вы получили аккуратно отформатированный список окружения и содержимого формы (в этом примере поля должны быть перечислены как «addr» со значением «At Home» и «name» со значением «Joe Blow»), то скрипт cgi.py установлен правильно. Если вы выполните ту же процедуру для своего скрипта, то сможете его отладить.
Следующим шагом может быть вызов функции test() модуля cgi из вашего скрипта: замените его основной код одним оператором
cgi.test()
Это должно дать те же результаты, что и установка самого файла cgi.py.
Когда обычный скрипт Python вызывает необработанное исключение (по любой причине: опечатка в имени модуля, файл, который не удаётся открыть, и т.п.), интерпретатор Python печатает подробную трассировку и завершает работу. Хотя интерпретатор Python всё ещё делает это, когда ваш CGI-скрипт вызывает исключение, скорее всего, трассировка попадёт в один из журнальных файлов HTTP-сервера или вообще будет отброшена.
К счастью, как только вам удалось заставить скрипт выполнить какой-либо код, вы можете легко отправлять трассировки в веб-браузер с помощью модуля cgitb. Если вы ещё этого не сделали, просто добавьте строки:
import cgitb
cgitb.enable()
в начало скрипта. Затем попробуйте запустить его снова; при возникновении проблемы вы увидите подробный отчёт, который, вероятно, укажет на причину сбоя.
Если вы подозреваете, что может быть проблема с импортом модуля cgitb, можно использовать ещё более надёжный подход (использующий только встроенные модули):
import sys
sys.stderr = sys.stdout
print("Content-Type: text/plain")
print()
...your code here...
Этот подход полагается на интерпретатор Python для печати трассировки. Тип содержимого вывода устанавливается в обычный текст, что отключает всю обработку HTML. Если ваш скрипт работает, сырой HTML будет отображён клиентом. Если он вызывает исключение, скорее всего, после вывода первых двух строк будет показана трассировка. Поскольку интерпретация HTML не происходит, трассировка будет читаемой.
20.2.9. Типичные проблемы и их решения¶Common problems and solutions
- Большинство HTTP-серверов буферизуют вывод CGI-скриптов до завершения скрипта. Это означает, что невозможно отображать индикатор выполнения на экране клиента во время работы скрипта.
- Проверьте инструкции по установке выше.
- Проверьте файлы журнала HTTP-сервера. (tail -f logfile в отдельном окне может быть полезно!)
- Всегда сначала проверяйте скрипт на синтаксические ошибки, выполнив что-то вроде python script.py.
- Если в вашем скрипте нет синтаксических ошибок, попробуйте добавить import cgitb; cgitb.enable() в начало скрипта.
- При вызове внешних программ убедитесь, что они доступны. Обычно это означает использование абсолютных путей – PATH обычно не настроен на что-то полезное в CGI-скрипте.
- При чтении или записи внешних файлов убедитесь, что они могут быть прочитаны или записаны тем идентификатором пользователя (userid), под которым будет выполняться ваш CGI-скрипт: обычно это идентификатор пользователя, под которым работает веб-сервер, или явно указанный идентификатор для функции suexec веб-сервера.
- Не пытайтесь установить CGI-скрипту режим set-uid. На большинстве систем это не работает и к тому же создаёт угрозу безопасности.
Сноски
| [1] | Обратите внимание, что некоторые последние версии спецификации HTML действительно определяют порядок, в котором должны передаваться значения полей, но определение того, был ли запрос получен от соответствующего спецификации браузера, или вообще от браузера, – задача утомительная и чреватая ошибками. |