Руководство по Python Bottle (Перевод) Часть 4 - Создание контента
В предыдущей части (Руководство по Python Bottle (Перевод) Часть 3 — Маршрутизация запросов) мы рассмотрели основные аспекты касающиеся маршрутизации запросов, в этой части будут рассмотрены аспекты относящиеся к передаваемому контенту.
Создание контента
В чистом стандарте WSGI диапазон возвращаемых из приложения типов очень ограничен. Приложения должны возвращать строки с возможностью разбиения их на набор байт. Можно вернуть строку (разбив её на части), но большинство серверов это заставляет передавать контент символ за символом. Строки в Unicode не допускаются вообще. Это не очень практично.
Bottle гораздо более гибок и поддерживает широкий спектр типов. Он автоматически добавляет заголовок Content-Length, когда это возможно, и автоматически кодирует Unicode, что облегчает работу разработчика. Ниже приведен список типов данных, которые могут быть возвращены из приложения, и краткое описание того, как они обрабатываются платформой:
Словари (Dictionaries)
Словари Python (или их подклассы) автоматически преобразуются в строки JSON и возвращаются в браузер с заголовком Content-Type, установленным в application/json. Это облегчает реализацию API на основе json. Однако, поддерживаются также форматы данных, и отличные от json.
Пустые строки, ложные, пустые и другие неверные значения (Empty Strings, False, None / non-true values)
Производят пустой вывод с заголовком Content-Length, установленным в 0.
Строки Unicode (Unicode strings)
Строки Unicode (или итерации, приводящие к строкам Unicode) автоматически кодируются с помощью кодека, указанного в заголовке Content-Type (по умолчанию utf8), а затем обрабатываются как обычные байтовые строки.
Байтовые строки (Byte strings)
Bottle возвращает строки целиком (вместо итерации по каждому символу) и добавляет заголовок Content-Length, основанный на длине строки. Списки байтовых строк объединяются первыми. Другие итерируемые строки с байтовыми строками не объединяются, потому что они могут стать слишком большими, и не поместиться в память. Заголовок Content-Length в этом случае не устанавливается.
Экземпляры HTTPError или HTTPResponse (HTTPError, HTTPResponse)
Из возвращение имеет тот же эффект, что и при их вызове в качестве исключения. В случае HTTPError применяется обработчик ошибок.
Файловые объекты (File)
Все, что имеет метод .read(), обрабатывается как файл или подобный файлу объект и передается в вызываемый объект wsgi.file_wrapper, определенный серверной средой WSGI. Некоторые реализации сервера WSGI могут использовать оптимизированные системные вызовы (sendfile) для более эффективной передачи файлов. В других случаях просто перебираются фрагменты, которые помещаются в память. При этом дополнительные заголовки, такие как Content-Length или Content-Type, автоматически не устанавливаются. Необходимо использовать send_file(), если это возможно.
Итераторы и генераторы (Iterables & generators)
Допускается использовать yield в вызовах или возвращать итерируемое, если итерация дает байтовые строки, строки юникода, экземпляры HTTPError или HTTPResponse. Вложенные итерации не поддерживаются. Следует обратить внимание, что код состояния HTTP и заголовки отправляются в браузер, как только итерация возвращает свое первое непустое значение.
Сортировка приведенного списка является важной. Например, вы можете вернуть подкласс str с помощью метода read(). При этом он все еще будет обрабатывается как строка вместо файла, потому что сначала обрабатываются строки.
Изменение кодировки по умолчанию (Default Encoding)
Bottle использует параметр charset заголовка Content-Type, чтобы определить как кодировать строки Unicode. По умолчанию заголовок имеет значение text/html; charset=UTF8 и может быть изменен с помощью атрибута Response.content_type или путем непосредственной установки атрибута Response.charset.
from bottle import response
@route('/iso')
def get_iso():
response.charset = 'ISO-8859-15'
return u'This will be sent with ISO-8859-15 encoding.'
@route('/latin9')
def get_latin():
response.content_type = 'text/html; charset=latin9'
return u'ISO-8859-15 is also known as latin9.'
В некоторых редких случаях когда имена кодирования Python отличаются от имен, поддерживаемых спецификацией HTTP, необходимо выполнить оба действия: сначала установить заголовок Response.content_type (который отправляется клиенту без изменений), а затем установить атрибут Response.charset (который используется для кодирования Unicode).
Статические Файлы (STATIC FILES)
Можно напрямую возвращать файлы, но метод static_file() является рекомендуемым способом обслуживания статических файлов. Он автоматически определяет MIME-тип, добавляет заголовок Last-Modified, ограничивает пути к корневому каталогу по соображениям безопасности и генерирует соответствующие сообщения об ошибках (403 при ошибках доступа, 404 при отсутствующих файлах). Этот метод даже поддерживает заголовок If-Modified-Since и генерирует ответ 304 Not Modified. Однако можно определить самостоятельно пользовательский тип MIME, чтобы отключить определение.
from bottle import static_file
@route('/images/<filename:re:.*\.png>')
def send_image(filename):
return static_file(filename, root='/path/to/image/files', mimetype='image/png')
@route('/static/<filename:path>')
def send_static(filename):
return static_file(filename, root='/path/to/static/files')
Можно вернуть возвращаемое значение static_file() как исключение, если это необхолдимо.
Принудительн6ая загрузка (Forced Download)
Большинство браузеров пытаются открыть загруженные файлы, если тип MIME известен и и сопоставлен с приложением (например, файлы PDF). Если этого не достаточно, то можно вызвать диалог загрузки и даже предложить пользователю имя файла:
@route('/download/<filename:path>')
def download(filename):
return static_file(filename, root='/path/to/static/files', download=filename)
Если параметр загрузки имеет значение True, используется исходное имя файла.
HTTP Ошибки и перенаправления (HTTP ERRORS AND REDIRECTS)
Функция abort() является ссылкой для генерации страниц ошибок HTTP.
from bottle import route, abort
@route('/restricted')
def restricted():
abort(401, "Извините, доступ запрещён.")
Чтобы перенаправить клиента на другой URL-адрес, вы можете отправить (303 See Other response with the Location), установленным на новый URL-адрес. redirect() сделает это за вас:
from bottle import redirect
@route('/wrong/url')
def wrong():
redirect("/right/url")
Вы можете указать другой код состояния HTTP в качестве второго параметра.
Примечание: Обе функции прервут ваш метод, вызвав исключение HTTPError.
Другие исключения
Все исключения, кроме HTTPResponse или HTTPError, приведут к ответу 500 Internal Server Error, поэтому они не будут аварийно завершать работу сервера WSGI. Вы можете отключить такое поведение для обработки исключений в вашем промежуточном программном обеспечении, установив bottle.app().Catchall в False.
Ответ сервера (THE RESPONSE OBJECT)
Метаданные ответа, такие как код состояния HTTP, заголовки ответа и файлы cookie, хранятся в объекте, называемом Response, до момента, когда они передаются в браузер. Можно напрямую манипулировать этими метаданными или использовать для этого предопределенные вспомогательные методы. Здесь описаны наиболее распространенные варианты использования и функции.
Код состояния (Status Code)
Код состояния HTTP контролирует поведение браузера и по умолчанию имеет значение 200 OK. В большинстве сценариев нет необходимости устанавливать атрибут Response.status вручную, однако использование метода abort() поможет вернуть экземпляр HTTPResponse с соответствующим кодом состояния. Допускается любое целое число, но коды, отличные от тех, которые определены в спецификации HTTP, только запутают браузер и нарушат стандарты.
Заголовок ответа (Response Header)
Заголовки ответа, такие как Cache-Control или Location, определяются через Response.set_header(). Этот метод принимает два параметра, имя заголовка и значение. Часть c именем нечувствительна к регистру:
@route('/wiki/<page>')
def wiki(page):
response.set_header('Content-Language', 'en')
...
Большинство заголовков уникальны, это означает, что клиенту отправляется только один заголовок на имя. Однако некоторые специальные заголовки могут появляться более одного раза в ответе. Чтобы добавить дополнительный заголовок, необходимо использовать Response.add_header() вместо Response.set_header():
response.set_header('Set-Cookie', 'name=value')
response.add_header('Set-Cookie', 'name2=value2')
Это всего лишь пример. Подробнее о работе с Cookie будет описано ниже.
Куки (COOKIES)
Cookie — это именованный фрагмент текста, который хранится в профиле браузера пользователя. Можно получить доступ к ранее определенным файлам cookie с помощью Request.get_cookie() и установить новые файлы cookie с помощью Response.set_cookie():
@route('/hello')
def hello_again():
if request.get_cookie("visited"):
return "Добро пожаловать! Рады вновь вас увидеть"
else:
response.set_cookie("visited", "yes")
return "Привет! Рады вас видеть"
Метод Response.set_cookie() принимает ряд дополнительных ключевых аргументов, которые управляют временем жизни и поведением Cookie. Некоторые из наиболее распространенных настроек описаны здесь:
max_age: максимальное время жизни в секундах. (по умолчанию: нет)
expires: объект datetime или отметка времени UNIX. (по умолчанию: нет)
domain: домен, который может читать куки. (по умолчанию: текущий домен)
path: ограничение файла cookie указанным путем (по умолчанию: /)
secure: ограничить использование cookie-подключений HTTPS (по умолчанию: отключено).
httponly: запретить jаvascript-файлу на стороне клиента читать этот файл cookie (по умолчанию: отключено, требуется Python 2.7 или более поздняя версия).
same_site: отключает стороннее использование cookie. Разрешенные атрибуты: слабый (lax) и строгий (strict). В строгом режиме куки никогда не будут отправлены. В слабом режиме куки отправляются только с GET-запросом верхнего уровня.
Если ни expires, ни max_age не установлены, срок действия cookie истекает в конце сеанса браузера или сразу после закрытия окна браузера. Есть и другие особенности, которые необходимо учитывать при использовании куки:
- Размер файлов cookie ограничен 4 КБ текста в большинстве браузеров.
- Некоторые пользователи настраивают свои браузеры так, чтобы они вообще не принимали файлы cookie. Большинство поисковых систем игнорируют куки тоже. Необходимо убедиться, что приложение по-прежнему работает без файлов cookie.
- Файлы cookie хранятся на стороне клиента и не шифруются никоим образом. Все, что хранится в куки, пользователь может прочитать. Хуже того, злоумышленник может украсть куки пользователя с помощью уязвимостей XSS. Известно, что некоторые вирусы читают куки браузера. Таким образом, никогда не стоит хранить конфиденциальную информацию в файлах cookie.
- Файлы cookie легко подделываются вредоносными клиентами. Не стоит доверять куки.
Подписанные cookie (Signed Cookies)
Как уже упоминалось выше, cookie-файлы легко подделываются вредоносными клиентами. Bottle может криптографически подписывать куки, чтобы предотвратить подобные манипуляции. Все, что необходимо сделать, это предоставить ключ подписи с помощью аргумента секретного ключевого слова всякий раз, когда вы читаете или устанавливаете cookie, и сохранить этот ключ в секрете. В результате Request.get_cookie() вернет None, если cookie не подписан или ключи подписи не совпадают:
@route('/login')
def do_login():
username = request.forms.get('username')
password = request.forms.get('password')
if check_login(username, password):
response.set_cookie("account", username, secret='some-secret-key')
return template("<p>Добро пожаловать {{name}}! Вы не авторизованы.</p>", name=username)
else:
return "<p>Авторизация не удалась.</p>"
@route('/restricted')
def restricted_area():
username = request.get_cookie("account", secret='some-secret-key')
if username:
return template("Привет {{name}}. Добро пожаловать.", name=username)
else:
return "Вы не авторизованы. Доступ запрещён."
Кроме того, Bottle автоматически выбирает и распаковывает любые данные, хранящиеся в подписанных файлах cookie. Это позволяет вам сохранять любой объект, к которому применяется подпись, в файлы cookie, подписанные данные которого не превышают ограничение в 4 КБ.
Предупреждение:
Подписанные файлы cookie не шифруются (клиент по-прежнему может просматривать содержимое) и не защищены от копирования (клиент может восстановить старый файл cookie). Основное цель состоит в том, чтобы сделать открепление безопасным и предотвратить манипуляции, а не хранить секретную информацию на стороне клиента.