CDKとAPI Gatewayと複数設定されたCORSのOriginについて

# 依頼内容
CORSのoriginsを下記の通り受け付けるようにして！

`https://somedomain.com`
`https://*.somedomain.com` 、たとえば `https://sub.somedomain.com` こんな感じのオリジン全部。

## どうしよう？
プリフライト時の(OPTIONS) メソッドでリクエストきたときの `access-control-allow-origin` ヘッダのことですね・・
えーとまずRFC的にどう書くんだっけか。

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin

```
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: <origin>
Access-Control-Allow-Origin: null
```

では・・originって何？

RFC 6454より。

```
   origin              = "Origin:" OWS origin-list-or-null OWS
   origin-list-or-null = %x6E %x75 %x6C %x6C / origin-list
   origin-list         = serialized-origin *( SP serialized-origin )
   serialized-origin   = scheme "://" host [ ":" port ]
                       ; <scheme>, <host>, <port> from RFC 3986
```

BNF表記めっちゃ久しぶりだけど・・origin-listはスペースで区切って好きなだけ並べるがいいって感じよね。

では、hostって何？ RFC 3986見ろって書いてあるので・・

https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2

```
  host        = IP-literal / IPv4address / reg-name
  ...
  reg-name    = *( unreserved / pct-encoded / sub-delims )
```

hostの前者２つがIPアドレスで、このreg-nameというのがホスト名の書式。

```
   pct-encoded   = "%" HEXDIG HEXDIG
   unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
   sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
                 / "*" / "+" / "," / ";" / "="
```

`*` ありますねーーーー！

じゃあ設定してもいいんだ。

と思って設定しようとするとAPI Gatewayが変な感じ。

# CDKはAPI Gatewayで何をしようとしているのか？
まずはoriginを１個だけ設定した状態から、複数設定したときのdiffを御覧いただきたい。

```
[~] AWS::ApiGateway::Method Resource-watch/OPTIONS ResourcewatchOPTIONS3348F02A 
 └─ [~] Integration
     └─ [~] .IntegrationResponses:
         └─ @@ -1,13 +1,16 @@
            [ ] [
            [ ]   {
            [ ]     "ResponseParameters": {
            [ ]       "method.response.header.Access-Control-Allow-Origin": "'https://somedomain.com'",
            [ ]       "method.response.header.Vary": "'Origin'",
            [ ]       "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'",
            [ ]       "method.response.header.Access-Control-Max-Age": "'7200'",
            [ ]       "method.response.header.Access-Control-Expose-Headers": "''"
            [ ]     },
            [+]     "ResponseTemplates": {
            [+]       "application/json": "#set($origin = $input.params().header.get(\"Origin\"))\n#if($origin == \"\") #set($origin = $input.params().header.get(\"origin\")) #end\n#if($origin == \"https://*.somedomain\")\n  #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)\n#end"
            [+]     },
            [ ]     "StatusCode": "204"
            [ ]   }
            [ ] ]

```

## パターンその１
```
...
> Origin: https://somedomain.com
> 
< HTTP/2 204 
< date: ***
< access-control-allow-origin: https://somedomain.com
< access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent
< vary: Origin
< access-control-allow-methods: OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD
< access-control-expose-headers: 
< access-control-max-age: 7200
```

オッケー

## パターンその２
```
...
> Origin: https://sub.somedomain.com
> 
< HTTP/2 204 
< date: ***
< x-amzn-requestid: ***
< access-control-allow-origin: https://somedomain.com
< access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent
< vary: Origin
< access-control-allow-methods: OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD
< access-control-expose-headers: 
< access-control-max-age: 7200
```

ダメ。
`access-control-allow-origin` が設定されない。

でもdiffをよく見ると・・・なんでレスポンステンプレートを設定しようとする？

```
            [+]     "ResponseTemplates": {
            [+]       "application/json": "#set($origin = $input.params().header.get(\"Origin\"))\n#if($origin == \"\") #set($origin = $input.params().header.get(\"origin\")) #end\n#if($origin == \"https://*.somedomain\")\n  #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)\n#end"
            [+]     },
```

このレスポンステンプレートでやろうとしていること。

「リクエストの `Origin` ヘッダの値を読んで判定しレスポンスの `Access-Control-Allow-Origin` ヘッダの内容を決めよう、このヘッダには一つしか 値 を設定しないからね。」

そしてその判定条件は**Originと設定したOriginsの要素の完全一致**。
アスタリスクで `Origin` 飛ばしてくるブラウザなんてないから・・・・このままではダメ。

証拠にリクエストの `Origin`ヘッダにアスタリスクぶっこんで完全一致するように仕向けると。

```
> Origin: https://*.somedomain.com
> 
< HTTP/2 204 
< date: Sun, 25 Aug 2024 *** GMT
< access-control-allow-origin: https://*.somedomain.com
< access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent
< vary: Origin
< access-control-allow-methods: OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD
< access-control-expose-headers: 
< access-control-max-age: 7200
```

わろた。

まーブラウザがアスタリスクで書いてある `access-control-allow-origin` ヘッダの値受け取っても正しく解釈するとは限らんしな、丁寧に１個だけ返してあげようという日本人的な配慮が感じられる。

# どうしましょうかね
現状のコードは下記。

```
        const apiWatch = new Resource(this, "Resource-xyz", {
            parent: api,
            pathPart: "xyz",
            defaultCorsPreflightOptions: {
                allowOrigins: ["https://somedomain.com", "https://*.somedomain.com"],
                allowHeaders: Cors.DEFAULT_HEADERS,
                exposeHeaders: [],
                maxAge: Duration.seconds(7200)
            },
        });
```

CORS関連の部分は用意してある設定を用いると手が出せない。
自前でOPTIONSヘッダ実装すればなんとかなると思う。

仕様としては、リクエストの `Origin` がパターンにマッチするんだったらそのまま Origin の値を レスポンスの `Access-Control-Allow-Origin` ヘッダに転写して返す、だ。

CDKのテンプレートに変換されたコードをそのまま真似してOPTIONSメソッド実装すればいいと思うんだけどそれだけではアスタリスクの判定がうまくいかない・・はず。

レスポンステンプレートの中でなんとかできる、か？

レスポンステンプレート(VTL)のリファレンス
https://velocity.apache.org/engine/2.0/vtl-reference.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570665/5fc39812-3aee-ed44-4d26-8d0b2a0ff4a0.png)

正規表現とかだめですねこれはｗ
ページ内探しても正規表現に関する言及がない。

さーどうしようかなっと・・

# 参照
この辺で議論されている厄ネタ。
https://github.com/aws/aws-cdk/issues/30453

で多分バグではなくって仕様だけど、RFC上複数のoriginをスペースで並べて返していいってなってるからResponseTemplateで変なことしなくていいんじゃない？って思うんだけどなんか理由でもあるのかな。