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

Исходный код: Lib/cgi.py.

Не рекомендуется, начиная с версии 3.11: Модуль cgi устарел (подробности и альтернативы см. в PEP 594).


Модуль поддержки сценариев Common Gateway Interface (CGI).

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

Введение

CGI-скрипт вызывается HTTP-сервером, обычно для обработки пользовательского ввода, представленного через элемент HTML <FORM> или <ISINDEX>.

Чаще всего CGI-скрипты живут в специальном каталоге сервера cgi-bin. HTTP-сервер помещает всевозможную информацию о запросе (например, имя хоста клиента, запрашиваемый URL, строку запроса и множество других данных) в среду оболочки скрипта, выполняет скрипт и отправляет его результаты обратно клиенту.

Ввод скрипта также подключен к клиенту, и иногда данные формы считываются таким образом; в других случаях данные формы передаются через «строку запроса» части URL. Этот модуль призван позаботиться о различных случаях и обеспечить более простой интерфейс для скрипта Python. Он также предоставляет ряд утилит, которые помогают в отладке скриптов, и последнее дополнение - поддержка загрузки файлов из формы (если ваш браузер поддерживает это).

Вывод CGI-скрипта должен состоять из двух разделов, разделенных пустой строкой. Первая секция содержит ряд заголовков, сообщающих клиенту, какого рода данные следуют далее. Код Python для генерации минимальной секции заголовков выглядит следующим образом:

print("Content-Type: text/html")    # HTML is following
print()                             # blank line, end of headers

Второй раздел обычно представляет собой HTML, который позволяет клиентскому программному обеспечению отображать красиво отформатированный текст с заголовком, встроенными изображениями и т.д. Вот код Python, который печатает простой фрагмент HTML:

print("<TITLE>CGI script output</TITLE>")
print("<H1>This is my first CGI script</H1>")
print("Hello, world!")

Использование модуля 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(). Поля формы, содержащие пустые строки, игнорируются и не появляются в словаре; чтобы сохранить такие значения, при создании экземпляра FieldStorage укажите значение true для необязательного параметра ключевого слова keep_blank_values.

Например, следующий код (который предполагает, что заголовок 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(), который всегда возвращает список значений (так что вам не нужно выделять случай с одним элементом). Например, этот код объединяет любое количество полей имени пользователя, разделенных запятыми:

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

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

fileitem = form["userfile"]
if fileitem.file:
    # It's an uploaded file; count lines
    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-тип, соответствующий 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):
    # The user is requesting more than one item.
else:
    # The user is requesting only one item.

Такая ситуация часто встречается, например, когда форма содержит группу из нескольких флажков с одинаковым именем:

<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 не существует такого поля формы или значения. Он возвращает список, состоящий из одного элемента, если существует только одно такое значение.

Используя эти методы, вы можете написать хороший компактный код:

import cgi
form = cgi.FieldStorage()
user = form.getfirst("user", "").upper()    # This way it's safe.
for item in form.getlist("item"):
    do_something(item)

Функции

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

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

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

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.10: Добавлен параметр 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() или другие функции с похожей функциональностью), убедитесь, что вы не передаете в shell произвольные строки, полученные от клиента. Это хорошо известная брешь в системе безопасности, благодаря которой ловкие хакеры в любой точке Интернета могут использовать доверчивый CGI-скрипт для вызова произвольных команд командного интерпретатора. Нельзя доверять даже части URL или именам полей, поскольку запрос не обязательно должен исходить от вашей формы!

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

Установка вашего CGI-скрипта в системе Unix

Прочитайте документацию к вашему HTTP-серверу и уточните у локального системного администратора каталог, в который должны быть установлены CGI-скрипты; обычно он находится в каталоге cgi-bin в дереве сервера.

Убедитесь, что ваш сценарий доступен для чтения и исполнения «другими»; режим файла Unix должен быть 0o755 восьмеричным (используйте chmod 0755 filename). Убедитесь, что первая строка сценария содержит #!, начиная с колонки 1, за которой следует имя пути интерпретатора 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-сценариях).

Тестирование вашего 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 печатает красивый traceback и выходит из программы. Хотя интерпретатор 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 для печати обратного хода. Тип содержимого вывода установлен на plain text, что отключает всю обработку 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 указано, в каком порядке должны быть представлены значения полей, но узнать, был ли запрос получен от соответствующего браузера или вообще от браузера, довольно утомительно и чревато ошибками.

Вернуться на верх