しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Static Web Apps のバックエンド API 側で実際にアクセスされているホスト名を取得する

Static Web Apps には組み込みの Azure Functions や独自の App Service や Azure Functions を /api 以下にリンクする機能があるので、同一ホスト名で SPA と API を動かすことが出来るのが大きな特徴です。

同一ホスト上で動いているようにブラウザからは見えますが、実体としては SPA 部分と API 部分は別々にホストされているので、現在ブラウザからアクセスされているホスト名を取る場合には注意が必要になります。例えば以下のようなコードを書いて API からホスト名を取得してみます。

[Function("HttpTrigger1")]
public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
{
    var originalHost = req.Host.Host;

    return new OkObjectResult(originalHost);
}

実際に Static Web App にデプロイをしてみると、以下のように Azure Functions がデプロイされているホスト名が返ってきます。Managed Functions の場合は自動生成されたホスト名が、API リンクを使った場合はリンク先のホスト名が返ってきています。カスタムドメインを設定していても考慮されることはありません。

ASP.NET Core は X-Forwarded-Host を適切にハンドリングしてくれる機能が用意されていますが、有効化しても Azure Functions 側で取れるホスト名には変化がありません。原因としては、そもそも X-Forwarded-Host が API へのプロキシ時に付与されていないことにあります。

以下のような Issue が上がっていますが、特に動きが無いので直近で対応されることは無さそうです。

一般的にはアクセスされているホスト名は X-Forwarded-Host で受け取るものなので、何か代わりになるヘッダーを各環境で探してみたところ X-MS-ORIGINAL-URL の値が使えそうでした。このヘッダーには名前の通りアクセスされたオリジナルの URL が設定されているので、URL からホスト名を取り出せば良いです。

本来ならミドルウェアで Host プロパティの値をオーバーライドするべきなのでしょうが、Azure Functions のミドルウェアは ASP.NET Core のそれとは割と異なっていたので諦めてメソッドにしました。

private string GetOriginalHost(HttpRequest req)
{
    if (req.Headers.TryGetValue("X-MS-ORIGINAL-URL", out var value))
    {
        return new Uri(value).Host;
    }

    return req.Host.Host;
}

このメソッドを使ってホスト名を取得するように変更してデプロイすれば、以下のようにブラウザでアクセスしているホスト名と応答で返されるホスト名が一致していることが確認出来ます。

但し Front Door のような L7 のリバースプロキシを更に前段に置いている場合は forwardingGateway の設定が必要になるので注意してください。

将来的に挙動が変わる可能性は十分あるのですが、現時点では X-MS-ORIGINAL-URL の値以外にバックエンド API でアクセスされているホスト名を取る方法は存在していないので、将来的に X-Forwarded-Host などが追加されるまではこの値を使うしかないです。

ここから先は検証時に調べたヘッダーの値について書いているので、興味がある方だけ読んでもらえれば問題ありません。Front Door を利用した場合の動作の違いについても書いてあります。

各環境におけるヘッダーの値まとめ

全ての環境において共通して言えるのは、バックエンド API に渡されるヘッダーにホスト名を含んでいるものは以下のように多く存在していますが、どれも同じ値あるいは意味のない値のどちらかになるため、今回のユースケースでは使い物にならないということです。

  • DISGUISED-HOST
  • Host
  • WAS-DEFAULT-HOSTNAME
  • X-Original-Host

X-Original-* は Azure Functions が内部で使っているヘッダーのようで、ローカルで動かした場合でも付与されているので今回は役に立たないと思って問題ないはずです。

Managed Functions の場合

DISGUISED-HOST: cd08ad42-0995-47f9-84e7-3d0d3491ea40.azurewebsites.net
Host: cd08ad42-0995-47f9-84e7-3d0d3491ea40.azurewebsites.net
WAS-DEFAULT-HOSTNAME: cd08ad42-0995-47f9-84e7-3d0d3491ea40.azurewebsites.net
X-MS-ORIGINAL-URL: https://ashy-dune-0506d1c00.5.azurestaticapps.net/api/HttpTrigger1
X-Original-For: 127.0.0.1:51010
X-Original-Host: localhost:50371
X-Original-Proto: http
X-Original-URL: /api/HttpTrigger1
X-WAWS-Unencoded-URL: /api/HttpTrigger1

API リンクを行った Azure Functions の場合

DISGUISED-HOST: func-swa-api-byof.azurewebsites.net
Host: func-swa-api-byof.azurewebsites.net
WAS-DEFAULT-HOSTNAME: func-swa-api-byof.azurewebsites.net
X-MS-ORIGINAL-URL: https://proud-meadow-000b01500.5.azurestaticapps.net/api/HttpTrigger1
X-Original-For: 127.0.0.1:50006
X-Original-Host: localhost:49226
X-Original-Proto: http
X-Original-URL: /api/HttpTrigger1
X-WAWS-Unencoded-URL: /api/HttpTrigger1

SWA にカスタムドメインを設定した場合

DISGUISED-HOST: func-swa-api-byof.azurewebsites.net
Host: func-swa-api-byof.azurewebsites.net
WAS-DEFAULT-HOSTNAME: func-swa-api-byof.azurewebsites.net
X-MS-ORIGINAL-URL: https://static.hackazure.net/api/HttpTrigger1
X-Original-For: 127.0.0.1:50383
X-Original-Host: localhost:49226
X-Original-Proto: http
X-Original-URL: /api/HttpTrigger1
X-WAWS-Unencoded-URL: /api/HttpTrigger1

Front Door 経由でアクセスした場合

DISGUISED-HOST: 768235ea-a136-4ca9-bea1-c30ff53d56cd.azurewebsites.net
Host: 768235ea-a136-4ca9-bea1-c30ff53d56cd.azurewebsites.net
WAS-DEFAULT-HOSTNAME: 768235ea-a136-4ca9-bea1-c30ff53d56cd.azurewebsites.net
X-MS-ORIGINAL-URL: https://ashy-dune-0506d1c00.5.azurestaticapps.net/api/HttpTrigger1
X-Original-For: 127.0.0.1:49308
X-Original-Host: localhost:49267
X-Original-Proto: http
X-Original-URL: /api/HttpTrigger1
X-WAWS-Unencoded-URL: /api/HttpTrigger1

前述しましたが Static Web App の前に Front Door のような L7 のリバースプロキシを置いてしまうと、一般的には X-MS-ORIGINAL-URL の値は Static Web App のデフォルトドメインになってしまうため、アクセスされているホスト名を取ることが出来ません。

そこで staticwebapp.config.json を編集して forwardingGateway に対して Front Door 経由でアクセスされる際のホスト名を追加しておきます。この設定を追加すると SWA が X-Forwarded-Host を読み取って正しく扱ってくれるようになります。

{
  "forwardingGateway": {
    "allowedForwardedHosts": [
      "fde-swa-api-e0hzbbahbrctfvfc.z01.azurefd.net"
    ]
  }
}

この設定の主な目的は OIDC などで必要なリダイレクト URL を正しく生成するためなのですが、内部的には X-Forwarded-Host を正しく扱うというものなので X-MS-ORIGINAL-URL に含まれるホスト名についても以下のようにアクセスされているホスト名で生成されます。

DISGUISED-HOST: a9c06c17-7fb6-4d08-add8-19d8728b8dbd.azurewebsites.net
Host: a9c06c17-7fb6-4d08-add8-19d8728b8dbd.azurewebsites.net
WAS-DEFAULT-HOSTNAME: a9c06c17-7fb6-4d08-add8-19d8728b8dbd.azurewebsites.net
X-MS-ORIGINAL-URL: https://fde-swa-api-e0hzbbahbrctfvfc.z01.azurefd.net/api/HttpTrigger1
X-Original-For: 127.0.0.1:50615
X-Original-Host: localhost:50577
X-Original-Proto: http
X-Original-URL: /api/HttpTrigger1
X-WAWS-Unencoded-URL: /api/HttpTrigger1

内部的には X-Forwarded-Host を扱う実装が入っているのだから、API へのプロキシ時にも X-Forwarded-Host を渡して欲しさしかないです。プロキシを実装する際には気を付けたいですね。