cgi — Поддержка Общего Интерфейса Шлюза


Модуль поддержки сценариев Общего Интерфейса Шлюза (CGI).

Модуль определяет ряд утилит для использования CGI скриптами, написанными на языке Python.

Введение

Сценарий 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-скрипта</TITLE>")
print("<H1>Это мой первый CGI-скрипт</H1>")
print("Привет, мир!")

Использование cgi модуля

Начнём с написания 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>Ошибка</H1>")
    print("Пожалуйста, заполните поля имени и адреса.")
    return
print("<p>name:", form["name"].value)
print("<p>addr:", form["addr"].value)
...дальнейшая обработка формы...

Здесь поля, доступные через form[key], сами являются сущностями FieldStorage (или MiniFieldStorage, в зависимости от кодировки формы). Атрибут value сущности возвращает строковое значение поля. Метод getvalue() возвращает это строковое значение напрямую; он также принимает необязательный второй аргумент по умолчанию для возврата, если запрошенный ключ отсутствует.

Если отправленные данные в форме содержат больше чем одно поле с тем же именем, объект, запрашиваемый form[key], не является сущностью FieldStorage или MiniFieldStorage, а списком из такой сущности. Точно так же в этой ситуации form.getvalue(key) возвращает список строк. Если вы ожидаете такой возможности (когда HTML-форма содержит несколько полей с одинаковым именем), используйте метод getlist(), который всегда возвращает список значений (чтобы не было необходимости в особом случае отдельного элемента). Например, код объединяет любое количество полей имени пользователя, разделенных запятыми:

value = form.getlist("username")
usernames = ",".join(value)

Если поле представляет загруженный файл, доступ к значению через атрибут value или getvalue() метод читает весь файл в памяти в виде байтов. Возможно, вы этого не хотите. Проверить загруженный файл можно, проверив либо атрибут filename, либо атрибут file. Вы можете тогда прочитать данные из признака file, прежде чем это будет автоматически закрыто как часть сборки мусора FieldStorage сущность (read() и readline() методы возвращающие байты):

fileitem = form["userfile"]
if fileitem.file:
    # Это загруженный файл; считать строки
    linecount = 0
    while True:
        line = fileitem.file.readline()
        if not line: break
        linecount = linecount + 1

FieldStorage объекты также поддерживают оператор with, которая автоматически закроет их.

При обнаружении ошибки при получении содержимого загруженного файла (например, при прерывании пользователем отправки формы нажатием кнопки «Назад» или «Отмена») атрибут done объекта для поля будет содержать значение -1.

Черновой стандарт загрузки файлов обеспечивает возможность загрузки нескольких файлов из одного поля (используя рекурсивную кодировку multipart/*). В этом случае элемент будет выглядеть как словарь FieldStorage. Это можно определить, проверив его атрибут type, который должен быть multipart/form-data (или, возможно, другой MIME тип соответствия: mimetype: „multipart/*“). В этом случае его можно итерировать рекурсивно так же, как объект формы верхнего уровня.

При подаче формы в «старом» формате (в качестве строки запроса или как одиночной части данных типа application/x-www-form-urlencoded) элементы фактически будут сущностями класса MiniFieldStorage. В этом случае атрибуты list, file и filename всегда являются None.

Форма, отправленная через POST и содержащая строку запроса, будет содержать как FieldStorage, так и MiniFieldStorage элементы.

Изменено в версии 3.4: Атрибут file автоматически закрывается после сборки мусора создаваемой сущности FieldStorage.

Изменено в версии 3.5: Добавлена поддержка протокола управления контекстом для класса FieldStorage.

Высокоуровневый интерфейс

В предыдущем разделе объясняется, как считывать данные формы 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)

Функции

Они полезны, если требуется больше управления или если требуется использовать некоторые алгоритмы, реализованные в этом модуле в других случаях.

cgi.parse(fp=None, environ=os.environ, keep_blank_values=False, strict_parsing=False)

Парсит запрос в среде или из файла (по умолчанию это sys.stdin). Параметры keep_blank_values, strict_parsing и separator передаются в urllib.parse.parse_qs() без изменений.

Изменено в версии 3.8.8: Добавлен параметр separator.

cgi.parse_multipart(fp, pdict, encoding="utf-8", errors="replace", separator="&")

Парсинг входных данных типа multipart/form-data (для загрузки файлов). Аргументы fp для входного файла, pdict для словаря, содержащего другие параметры в заголовке Content-Type, и encoding кодировку запроса.

Возвращает словарь так же, как и urllib.parse.parse_qs(): ключи - это имена полей, каждое значение - это список значений для этого поля. Для нефайловых полей значением является список строки.

Ее легко использовать, но не очень хорошо, если вы ожидаете, что мегабайты будут загружены, — в таком случае, используйте FieldStorage класс вместо который гораздо более гибким.

Изменено в версии 3.7: Добавлены параметры encoding и errors. Для нефайловых полей значение теперь является списком строки, а не байтов.

Изменено в версии 3.8.8: Добавлен параметр separator.

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()

Печать списка полезных (используемых CGI) переменных среды в HTML.

Забота о безопасности

Есть одно важное правило: если вы вызываете внешнюю программу (через функции os.system() или os.popen() или другие с аналогичной функциональностью), убедитесь, что вы не передаете произвольную строки, полученную от клиента, оболочке. Это хорошо известная дыра безопасности, благодаря которой умные хакеры в любой точке сети могут использовать доверительный CGI-сценарий для вызова произвольных команд оболочки. Даже части URL-адреса или имен полей нельзя доверять, так как запрос не должен поступать из формы!

Для обеспечения безопасности при передаче строка, полученная из формы в команду оболочки, необходимо убедиться, что строка содержит только буквенно-цифровые символы, тире, подчеркивания и точки.

Установка CGI сценария в системе Unix

Ознакомьтесь с документацией для HTTP-сервера и обратитесь к системному администратору, чтобы найти каталог, в котором должны быть установлены сценарии CGI. Обычно это в каталоге, cgi-bin дерева каталогов сервера.

Убедитесь, что сценарий читается и выполняется «другими»; режим файлов Unix должен быть 0o755 восьмеричным (используйте chmod 0755 filename). Убедитесь, что первая строка сценария содержит #!, начинающиеся, Python, в столбце 1, за которым следует путь к интерпретатору:

#!/usr/local/bin/python

Убедитесь, что Python интерпретатор существует и выполняется другими пользователями.

Убедитесь, что все файлы, которые необходимо прочитать или записать, читаются или доступны для записи соответственно «другими» — их режим должен быть 0o644 для чтения и 0o666 для записи. Это связано с тем, что по соображениям безопасности HTTP-сервер выполняет сценарий как пользователь «nobody» без каких-либо специальных привилегий. Он может читать (записывать, исполнять) только те файлы, которые каждый может читать (записывать, исполнять). Текущий каталог во время выполнения также отличается (обычно это каталог cgi-bin сервера), а набор переменных среды также отличается от того, что получают при входе в систему. В частности, не следует рассчитывать на то, что для пути поиска исполняемых файлов (PATH) или пути поиска Python модуля (PYTHONPATH) в оболочке будет установлено какое-либо интересное значение.

Если необходимо загрузить модули из каталога, который не находится Python’s пути поиска модулей по умолчанию, перед импортом других модулей можно изменить путь в сценарии. Например:

import sys
sys.path.insert(0, "/usr/home/joe/lib/python")
sys.path.insert(0, "/usr/local/lib/python")

(Таким образом, вставленный последний каталог будет найденный первым!)

Инструкции для систем, отличных от Unix, будут различаться; проверьте документацию HTTP-сервера (обычно в нем имеется раздел о скриптах CGI).

Тестирование CGI сценария

К сожалению, сценарий CGI, как правило, не будет запускаться при попытке его выполнения из командной строки, и сценарий, который отлично работает из командной строки, может загадочно провалиться при запуске с сервера. Есть одна причина, почему вы всё ещё должны тестировать свой сценарий из командной строки: если он содержит синтаксическую ошибку Python интерпретатор не выполнит ее вообще, и HTTP-сервер, скорее всего, отправит клиенту зашифрованную ошибку.

Если в сценарии нет синтаксических ошибок, но он не работает, у вас нет другого выбора, кроме как прочитать следующий раздел.

Отладка CGI сценария

Прежде всего, проверьте на наличие тривиальных ошибок установки — тщательно прочитав раздел выше об установке сценария 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 установлен правильно. Если вы выполняете ту же процедуру для собственного сценария, то теперь вы должны иметь возможность его отладки.

Следующим шагом может стать вызов функции cgi модуля test() из сценария: замена его главный код одиночным оператором:

cgi.test()

Это должно привести к тем же результатам, что и при установке самого файла cgi.py.

Когда обычный сценарий Python вызывает необработанное исключение (по какой либо причине: опечатка в имени модуля, файл, который нельзя открыть и т.д.), Python интерпретатор печатает подробный трейсбэк и выходит. В то время как Python интерпретатор будет делать это, когда сценарий CGI вызовет исключение, скорее всего, трейсбэк окажется в одном из файлов журнала HTTP-сервера или будет полностью отброшено.

К счастью, после выполнения сценария для выполнения some кода можно легко отправить отслеживание в веб-браузер с помощью модуля cgitb. Если вы еще не сделали этого, просто добавьте строки:

import cgitb
cgitb.enable()

В верхней части сценария. Затем повторите попытку. При возникновении проблемы должен появиться подробный отчет, который, вероятно, сообщит очевидную причину аварии.

Если вы подозреваете, что при импорте модуля cgitb может возникнуть проблема, можно использовать еще более надежный подход (который использует только встроенные модули):

import sys
sys.stderr = sys.stdout
print("Content-Type: text/plain")
print()
...ваш код далее...

Это зависит от Python интерпретатор печати трейсбэка. Тип содержимого вывода устанавливается в обычный текст, что отключает всю обработку HTML. Если сценарий работает, сырой HTML отображается клиентом. Если при этом вызывается исключение, то, скорее всего, после печати первых двух строк будет отображен трейсбэк. Поскольку интерпретация HTML не выполняется, трейсбэк будет читаемым.

Общие проблемы и решения

  • Большинство серверов HTTP буферизуют выходные данные CGI сценариев до завершения сценария. Это означает, что невозможно отобразить отчёт о ходе выполнения на дисплее клиента во время выполнения сценария.
  • Проверьте инструкции по установке, приведенные выше.
  • Проверьте файлы журнала HTTP-сервера. (tail -f logfile в отдельном окне может быть полезно!)
  • Всегда проверяйте сценарий на наличие синтаксических ошибок, делая что-то вроде python script.py.
  • Если в сценарии нет синтаксических ошибок, попробуйте добавить import cgitb; cgitb.enable() в верхней части сценария.
  • При вызове внешних программ убедитесь, что они могут быть найдены. Обычно это означает использование абсолютных имён путей — PATH обычно не устанавливается на очень полезное значение в сценарии CGI.
  • При чтении или записи внешних файлов убедитесь, что они могут читаться или записываться userid выполняющим CGI сценарий: обычно это userid под которым выполняется веб-сервер или какой-либо явно указанный userid для функции suexec веб-сервера.
  • Не пытайтесь предоставить скрипту CGI режим set-uid. Это не работает в большинстве систем, а также является обязательством безопасности.

Сноски

[1]Обратите внимание, что в некоторых последних версиях HTML спецификации указано, что порядок полей значений должны быть предоставлены, но зная, является ли запрос полученным из соответствующего браузера или даже из браузера, является утомительно и подвержено ошибкам.