cgi — Підтримка загального інтерфейсу шлюзу

Вихідний код: Lib/cgi.py

Застаріло починаючи з версії 3.11: Модуль cgi є застарілим (перегляньте PEP 594 для деталей та альтернатив).


Модуль підтримки для сценаріїв загального інтерфейсу шлюзу (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()

This activates a special exception handler that will display detailed reports in the Web browser if any errors occur. If you’d rather not show the guts of your program to users of your script, you can have the reports saved to files instead, with code like this:

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

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. Метод повертає порожній список, якщо для 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.9.2: Додано параметр роздільник.

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.

Турбота про безпеку

There’s one important rule: if you invoke an external program (via os.system(), os.popen() or other functions with similar functionality), make very sure you don’t pass arbitrary strings received from the client to the shell. This is a well-known security hole whereby clever hackers anywhere on the Web can exploit a gullible CGI script to invoke arbitrary shell commands. Even parts of the URL or field names cannot be trusted, since the request doesn’t have to come from your form!

Щоб бути в безпеці, якщо вам потрібно передати рядок, отриманий із форми, до команди оболонки, ви повинні переконатися, що рядок містить лише буквено-цифрові символи, тире, підкреслення та крапки.

Встановлення сценарію CGI в систему Unix

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

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

Наступним кроком може бути виклик функції test() модуля cgi з вашого сценарію: замініть його основний код одним оператором

cgi.test()

Це має призвести до тих самих результатів, що й після встановлення самого файлу cgi.py.

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

Fortunately, once you have managed to get your script to execute some code, you can easily send tracebacks to the Web browser using the cgitb module. If you haven’t done so already, just add the lines:

import cgitb
cgitb.enable()

на початок вашого сценарію. Потім спробуйте запустити його знову; коли виникає проблема, ви повинні побачити докладний звіт, який, ймовірно, стане очевидною причиною збою.

Якщо ви підозрюєте, що може виникнути проблема з імпортом модуля cgitb, ви можете скористатися навіть більш надійним підходом (який використовує лише вбудовані модулі):

import sys
sys.stderr = sys.stdout
print("Content-Type: text/plain")
print()
...your code here...

Це залежить від інтерпретатора Python для друку трасування. Тип вмісту виводу встановлено як звичайний текст, що вимикає всю обробку HTML. Якщо ваш сценарій працює, ваш клієнт відображатиме необроблений HTML. Якщо це викликає виняток, швидше за все, після того, як перші два рядки будуть надруковані, відобразиться зворотне трасування. Оскільки інтерпретація HTML не відбувається, трасування буде доступним для читання.

Поширені проблеми та рішення

  • Більшість HTTP-серверів буферизують вихідні дані сценаріїв CGI, доки сценарій не буде завершено. Це означає, що неможливо відобразити звіт про виконання на дисплеї клієнта під час виконання сценарію.

  • Перегляньте інструкції зі встановлення вище.

  • Перевірте файли журналу HTTP-сервера. (tail -f logfile в окремому вікні може бути корисним!)

  • Завжди спочатку перевіряйте сценарій на синтаксичні помилки, виконавши щось на зразок python script.py.

  • Якщо ваш сценарій не має синтаксичних помилок, спробуйте додати import cgitb; cgitb.enable() у верхній частині сценарію.

  • Викликаючи зовнішні програми, переконайтеся, що їх можна знайти. Зазвичай це означає використання абсолютних назв шляхів — PATH зазвичай не має дуже корисного значення в сценарії CGI.

  • Під час читання або запису зовнішніх файлів переконайтеся, що їх можна прочитати або записати за ідентифікатором користувача, під яким запускатиметься ваш сценарій CGI: зазвичай це ідентифікатор користувача, під яким запущено веб-сервер, або якийсь явно вказаний ідентифікатор користувача для` веб-сервера. функція suexec`.

  • Не намагайтеся надати сценарію CGI режим set-uid. Це не працює в більшості систем, а також є проблемою безпеки.

Виноски

1

Зауважте, що в деяких нещодавніх версіях специфікації HTML зазначено, у якому порядку слід надавати значення полів, але знати, чи був отриманий запит від відповідного браузера, або навіть від браузера взагалі, є виснажливим і схильним до помилок.