FastAPIでX-Forwarded-Hostを用いたリダイレクト¶
ソースを見てみたい人向け¶
https://gist.github.com/attakei/6b308a2a7949746a8027fc0258f1de1c
なにこれ¶
FastAPI
用に作ったミドルウェアで、リクエスト時に X-Forwarded-Host
ヘッダーが付与されていた場合に、
その値を Host ヘッダーに上書きするものです。
FastAPI
用とはいっても、依存してるのは Starlette
だけなので、 Starlette
派生なら普通に動くと思います。
何ができるの?¶
FastAPI
が動いているサーバーのホスト名と、サービスのFQDNが違っているケースで、
いわゆる Slashes redirection が有効になっている際に、
そのリダイレクトもサービスのFQDNで行われるようになります。
コードを書いてみた背景とか¶
Firebase + Cloud Run の挙動¶
このサイトは現在実験を兼ねて、 Firebase Hosting
+ Cloud Run
の組み合わせで動作していて、
一部のURLは バックエンド設定した Cloud Run
サービスからレスポンスを返すようにしています。
Cloud Run
はDockerコンテナを使って「サービス」を提供する機能のため、ホスト名を持っています。
Firebase
から Cloud Run
への通信も最終的にはこのホスト名が使われ、
Firebase
のホスト名自体は、 X-Forwarded-Host
ヘッダー経由で渡るようになっています。
FastAPIの Slashes redirection¶
FastAPI
のルーティングは、 APIRouter
クラスが担っています。
そして、 APIRouter
のプロパティには redirect_slashes
があります。
これは、 プロパティが True
なら「末尾が /
となるルーティング設定」がすでに存在する際に、
/
末尾が足りないリクエストが来た場合に、 /
スラッシュを付与したURLへリダイレクトさせる機能です。
このとき、内部ではリダイレクト先のURLをスキーマから全部構築した状態で、用意します。
そして、その時の参照状情報として Host
ヘッダーを利用しています。
組み合わさるとどうなるか¶
上記2点が組み合わさると、 Cloud Runの視点では Host
ヘッダーはあくまで Cloud Run
自身のサービスFQDNとなるため、
「Firebaseのホスト名でredirect-slashesの条件を満たすと、Cloud Runのホスト名のURLでリダイレクト指示を出す」
という状況が発生してしまいます。
「なんのためのFirebaseなのか」といった感じですね。
中身的には、こんな感じです。
# flake8: noqa
class Router:
"""starlette.routing.Router から必要最低限のとこだけ抜粋"""
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
# 前略
if scope["type"] == "http" and self.redirect_slashes and scope["path"] != "/":
# リクエストパスが / で終わらない時に、リダイレクト可能性があるので / 付きのデータを用意
redirect_scope = dict(scope)
if scope["path"].endswith("/"):
redirect_scope["path"] = redirect_scope["path"].rstrip("/")
else:
redirect_scope["path"] = redirect_scope["path"] + "/"
# ルーティング情報からリダイレクト先とマッチする情報があるか探し、
# 見つかったら、リダイレクトレスポンスを返してしまうs
for route in self.routes:
match, child_scope = route.matches(redirect_scope)
if match != Match.NONE:
redirect_url = URL(
scope=redirect_scope
) # <= この中身がhostしかみない
response = RedirectResponse(url=str(redirect_url))
await response(scope, receive, send)
return
# 後略
どう対処したか¶
https://gist.github.com/attakei/6b308a2a7949746a8027fc0258f1de1c
ミドルウェアを用意してリクエスト処理に割り込みを行い、
「 X-Forwarded-Host
ヘッダーがあった時に限り、 Host
ヘッダーの中身を上書きする」
ようにしました。
1class ForwardedHostMiddleware:
2 def remap_headers(self, src: Headers, before: bytes, after: bytes) -> Headers:
3 """元のヘッダーリストから、
4 - 無関係なものはそのままコピーして
5 - 置換対象は一旦退避して
6 - 結果に応じて再追記する
7 """
8 remapped = []
9 before_value = None
10 after_value = None
11 for header in src:
12 k, v = header
13 if k == before:
14 before_value = v
15 continue
16 elif k == after:
17 after_value = v
18 continue
19 remapped.append(header)
20 if after_value:
21 remapped.append((before, after_value))
22 elif before_value:
23 remapped.append((before, before_value))
24 return remapped
弊害はあるか?¶
他に Host
ヘッダを元に何かを識別しているケースがあると、もしかしたら想定外の挙動をするかもしれません。
ただ自分の場合だと、アプリケーションの中身自体は極力 Host
ヘッダーに依存しないよう作っているので、
きっと害は無いと信じてます。
週報(2020-04-06-)
週報(2020-03-30-)