AllIsHackedOff

Just a memo, just a progress

KtorのCORSがchromeのPreflight Requestに対してうまく働かなくてハマった問題

経緯

Kotlinを書いているうち - 適度にシュッとしている点 - Nullableをハンドリングできる点 - Collection系の関数があるところ - IntelliJとの相性抜群 などに惹かれたのでAPIでも書いてみようと軽そうなKtorを触ることに。

発見した問題

ChromeからのCORSのリクエスト、特にPreflight Requestが通らない。 Chromeのインスペクタから Copy as cURLしてやると、下記のリクエストで 403 が返ってくる

curl 'http://localhost:8080/sample' -X OPTIONS -H 'Access-Control-Request-Method: GET' -H 'Origin: http://localhost:8878' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en,ja-JP;q=0.9,ja;q=0.8,en-GB;q=0.7,en-US;q=0.6' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' -H 'Accept: */*' -H 'Connection: keep-alive' -H 'Access-Control-Request-Headers: authorization,crossdomain,x-csrf-token' --compressed

解決法

公式ドキュメントを読んでもそれらしき内容はないし、ISSUEもない、ということでなぜ 403 なのかをコードを追って確認することに。 CORS - Servers - Ktor

    private suspend fun ApplicationCall.respondPreflight(origin: String) {
        if (!corsCheckRequestMethod() || !corsCheckRequestHeaders()) {
            respond(HttpStatusCode.Forbidden) // ここで 403が返ってるっぽい
            return
        }

        accessControlAllowOrigin(origin)
        accessControlAllowCredentials()
        response.header(HttpHeaders.AccessControlAllowMethods, methodsListHeaderValue)
        response.header(HttpHeaders.AccessControlAllowHeaders, headersListHeaderValue)
        accessControlMaxAge()
        respond(HttpStatusCode.OK)

    }

ので corsCheckRequestHeader を追うことに

    private fun ApplicationCall.corsCheckRequestHeaders(): Boolean {
        val requestHeaders = request.headers.getAll(HttpHeaders.AccessControlRequestHeaders)?.flatMap { it.split(",") }?.map { it.trim().toLowerCase() } ?: emptyList()

        return !requestHeaders.any { it !in headers }
    }

Access-Control-Request-Headers で指定されたヘッダに許可していないものが入っていないか調べているらしい。Chromeから送られるリクエストは 'Access-Control-Request-Headers: authorization,crossdomain,x-csrf-token' の3つのヘッダーを含んでいるが、これらはKtorのデフォルト設定だと許可されていない

    class Configuration {
        companion object {
            val CorsDefaultMethods = setOf(HttpMethod.Get, HttpMethod.Post, HttpMethod.Head)

            // https://www.w3.org/TR/cors/#simple-header
            val CorsDefaultHeaders: Set<String> = TreeSet(String.CASE_INSENSITIVE_ORDER).apply {
                addAll(listOf(
                        HttpHeaders.CacheControl,
                        HttpHeaders.ContentLanguage,
                        HttpHeaders.ContentType,
                        HttpHeaders.Expires,
                        HttpHeaders.LastModified,
                        HttpHeaders.Pragma
                ))
            }

見た感じデフォルトで許可すべきヘッダはw3cの仕様で決まっている?(ここは追っていない とにかく、許可ヘッダを足す必要があるため、若干ウォークアラウンド感があるが下記のように設定を追加し事なきをえた

           header("CrossDomain")
            header("X-CSRF-Token")

これがブラウザごとに異なる、とかだと結構設定が面倒なので、Ktor側でうまく吸収して欲しい...