Сделайте Flask url_for использовать схему "https" в балансировщике нагрузки AWS, не испортив SSLify

Недавно я добавил SSL-сертификат на мой webapp. Он развернут на Amazon Web Services, используя балансировщики нагрузки. Балансиры нагрузки работают как обратные прокси, обрабатывают внешние HTTPS и отправляют внутренний HTTP. Таким образом, весь трафик для моего приложения Flask - это HTTP, а не HTTPS, несмотря на то, что он является безопасным соединением.

Поскольку сайт был уже включен в сеть до миграции HTTPS, я использовал SSLify для отправки 301 PERMANENT REDIRECTS в HTTP-соединения. Он работает, несмотря на то, что все соединения являются HTTP, потому что обратный прокси устанавливает заголовок запроса X-Forwarded-Proto с исходным протоколом.

Проблема

url_for не интересует X-Forwarded-Proto. Он будет использовать my_flask_app.config['PREFERRED_URL_SCHEME'] когда схема недоступна, но во время запроса доступна схема. Схема HTTP соединения с обратным прокси.

Поэтому, когда кто-то подключается к https://example.com, он подключается к балансировщику нагрузки, который затем подключается к Flask с помощью http://example.com. Flask видит http и предполагает, что схема - это HTTP, а не HTTPS, поскольку она изначально была.

Это не проблема в большинстве url_for используемых в шаблонах, но любой url_for с _external=True будет использовать http вместо https. Лично я использую _external=True для rel=canonical так как я слышал, что это рекомендуется. Кроме того, использование Flask.redirect приведет к добавлению внешних ссылок с помощью http://example.com, поскольку заголовок перенаправления должен быть полным URL-адресом.

Если вы перенаправите, например, сообщение о форме, это произойдет.

  1. Сообщения клиентов https://example.com/form
  2. Сервер выдает 303 SEE OTHER на http://example.com/form-posted
  3. Затем SSLify выдает 301 PERMANENT REDIRECT на https://example.com/form-posted

Каждое перенаправление становится 2 перенаправлением из-за SSLify.

Попытки решения

Добавление конфигурации PREFERRED_URL_SCHEME

qaru.site/info/290165/...

my_flask_app.config['PREFERRED_URL_SCHEME'] = 'https'

Не работает, потому что во время запроса есть схема, и вместо этого используется. См. Https://github.com/mitsuhiko/flask/issues/1129#issuecomment-51759359

Обшивка промежуточного программного обеспечения для издевательства HTTPS

qaru.site/info/290165/...

def _force_https(app):
    def wrapper(environ, start_response):
        environ['wsgi.url_scheme'] = 'https'
        return app(environ, start_response)
    return wrapper
app = Flask(...)
app = _force_https(app)

Как это было, это не сработало, потому что мне нужно было это приложение позже. Поэтому вместо этого я использовал wsgi_app.

def _force_https(wsgi_app):
    def wrapper(environ, start_response):
        environ['wsgi.url_scheme'] = 'https'
        return wsgi_app(environ, start_response)
    return wrapper
app = Flask(...)
app.wsgi_app = _force_https(app.wsgi_app)

Поскольку wsgi_app вызывается перед любыми обработчиками app.before_request, это делает SSLify причиной того, что приложение уже находится за защищенным запросом, а затем он не будет перенаправлять HTTP-HTTPS.

Исправление url_for

(Я даже не могу найти, откуда у меня это)

from functools import partial
import Flask
Flask.url_for = partial(Flask.url_for, _scheme='https')

Это может сработать, но Flask выдаст ошибку, если вы установите _scheme но не _external. Поскольку большинство моих приложений url_for являются внутренними, оно вообще не работает.

Ответ 1

У меня были такие же проблемы с "redirect" (url_for ('URL')) за базовым балансиром нагрузки AWS, и я решил это с помощью вызова werkzeug.contrib.fixers.ProxyFix в моем коде. пример:

from werkzeug.contrib.fixers import ProxyFix
app = Flask(__name__)

app.wsgi_app = ProxyFix(app.wsgi_app)

ProxyFix(app.wsgi_app) добавляет поддержку HTTP-прокси к приложению, которое не было разработано с использованием HTTP-прокси. Он устанавливает REMOTE_ADDR, HTTP_HOST из заголовков X-Forwarded.

Ответ 2

Выкапывая исходный код Flask, я узнал, что url_for использует Flask._request_ctx_stack.top.url_adapter когда есть контекст запроса.

url_adapter.scheme определяет используемую схему. Чтобы заставить параметр _scheme работать, url_for временно url_adapter.scheme и затем вернет его обратно, прежде чем функция вернется.

(это поведение обсуждалось в github относительно того, должно ли оно быть предыдущим значением или PREFERRED_URL_SCHEME)

В основном, что я сделал, был установлен url_adapter.scheme на https с обработчиком before_request. Этот способ не возится с самим запросом, только с вещью, генерирующей URL-адреса.

def _force_https():
    # my local dev is set on debug, but on AWS it not (obviously)
    # I don't need HTTPS on local, change this to whatever condition you want.
    if not app.debug: 
        from flask import _request_ctx_stack
        if _request_ctx_stack is not None:
            reqctx = _request_ctx_stack.top
            reqctx.url_adapter.url_scheme = 'https'

app.before_request(_force_https)