Cloudflare Transform Rules の適用動作を確認する

# Transform Rules とは

- [Transform Rules · Cloudflare Rules docs](https://developers.cloudflare.com/rules/transform/)

以下の4つのルールを定義して、各フェーズで実行することができます。

- [Rewrite URL rules](https://developers.cloudflare.com/rules/transform/url-rewrite/)
    - HTTPリクエストのURLパスとクエリー文字列を書き換える
- [HTTP request header modification rules](https://developers.cloudflare.com/rules/transform/request-header-modification/)
    - HTTPリクエスト・ヘッダーの値を設定するか、リクエスト・ヘッダーを削除する
- [HTTP response header modification rules](https://developers.cloudflare.com/rules/transform/response-header-modification/)
    - HTTPレスポンス・ヘッダーの値を設定するか、レスポンス・ヘッダーを削除する
- [Managed Transforms](https://developers.cloudflare.com/rules/transform/managed-transforms/)
    - ボタンをクリックするだけで、HTTPリクエストとレスポンスヘッダの一般的な調整を実行できる

# 適用順

- [Phases list · Cloudflare Ruleset Engine docs](https://developers.cloudflare.com/ruleset-engine/reference/phases-list/)

```mermaid:
flowchart LR
  subgraph CF[Cloudflare Transform Rules]
    Rewrite["Rewrite URL Rules<br>http_request_transform"] --- Managed1[Managed Transforms]
    Managed1["Managed Transforms<br>(Request)"] --- ReqHead["HTTP Request Header Modification Rules<br>http_request_late_transform"]
    ResHead["HTTP Response Header Modification Rules<br>http_response_headers_transform"] --- Managed2["Managed Transforms<br>(Response)"]
  end
  Eyeball[Eyeball] --- Rewrite & ResHead
  ReqHead & Managed2 --- Origin 
```

# 評価
以下のように評価されます。

- 順番に実行される
- 後の順番のルールは、前のルールによって行われた変更を上書きできる
- リクエストとレスポンスのフィールドは、リクエスト/レスポンスのルールを評価する間、各フェーズ内で不変

[Transform Rules evaluation](https://developers.cloudflare.com/rules/transform/#transform-rules-evaluation)
>Transform Rules run in order. 
Rules that appear later in the list of Transform Rules can overwrite changes done by previous rules. 
You can define the rule order in the dashboard or via API.
>
>Request and response fields are immutable within each phase while evaluating Transform Rules for a request/response.

# Rewrite URL rules の適用動作を確認
`https://example.com/photos/index.html?w=500&h=500` を対象に
Rewrite URL rules の適用動作を確認します。

- [Cloudflare からの HTTP リクエストヘッダを可視化する - Qiita](https://qiita.com/khayama/items/0787728497918f2e68d6)

以下のように HTTP request header modification rules で
適用前フィールド値 `raw.http.request.uri.path`, `raw.http.request.uri.query` と
適用後フィールド値 `http.request.uri.path`, `http.request.uri.query`
が確認できるように準備します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/71f43ea2-422b-b6b0-952d-9a55753d1301.png)

## `http.request.uri.query` のみリライト

### ルール1追加: `w` から `width` へリライト

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/518aa76b-abb4-3bda-0a31-f9f9fa8c73ae.png)

```shell:
% curl -s 'https://example.com/photos/index.html?w=500&h=500' | grep -e Query
Rewrite-Before-Raw-Http-Request-Uri-Query: w=500&h=500 <br />
Rewrite-After-Http-Request-Uri-Query: width=500&h=500 <br />
```

### ルール2追加: `h` から `height` へリライト

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/4085823e-9eaf-e5dd-90aa-4276bb2ba6a9.png)

```shell:
% curl -s 'https://example.com/photos/index.html?w=500&h=500' | grep -e Query
Rewrite-Before-Raw-Http-Request-Uri-Query: w=500&h=500 <br />
Rewrite-After-Http-Request-Uri-Query: w=500&height=500 <br />
```

この結果から、以下のことが確認できました。 

- どちらのルールも `http.request.uri.query` は元々のものを参照し、リライトフェーズを終えるまで変更を受けない
- ルール評価の結果、ルール2のみが結果として反映される


##  `http.request.uri.path` のみリライト

### ルール1追加: `/photos/` から `/photos/rewrite1/` へリライト

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/5518ae22-ce6b-3712-0760-2bfdd2383ff6.png)

```shell:
% curl -s 'https://example.com/photos/index.html?w=500&h=500' | grep -e Path
Rewrite-Before-Raw-Http-Request-Uri-Path: /photos/index.html <br />
Rewrite-After-Http-Request-Uri-Path: /photos/rewrite1/index.html <br />
```

### ルール2追加: `/photos/` から `/photos/rewrite2/` へリライト

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/44471da3-1985-c49d-5e8f-d0cd2d079875.png)

```shell:
% curl -s 'https://example.com/photos/index.html?w=500&h=500' | grep -e Path
Rewrite-Before-Raw-Http-Request-Uri-Path: /photos/index.html <br />
Rewrite-After-Http-Request-Uri-Path: /photos/rewrite2/index.html <br />
```

この結果から、クエリ同様に以下のことが確認できました。 

- どちらのルールも `http.request.uri.path` は元々のものを参照し、リライトフェーズを終えるまで変更を受けない
- ルール評価の結果、ルール2のみが結果として反映される

## パスとクエリを同時にリライト
上記で使用した4つのルールを以下のように並べ、適用動作を確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/b3ccfb2c-27fd-83ed-948c-21b3f754d13d.png)

```shell:
% curl -s 'https://example.com/photos/index.html?w=500&h=500' | grep -e Path -e Query
Rewrite-Before-Raw-Http-Request-Uri-Query: w=500&h=500 <br />
Rewrite-Before-Raw-Http-Request-Uri-Path: /photos/index.html <br />
Rewrite-After-Http-Request-Uri-Query: w=500&height=500 <br />
Rewrite-After-Http-Request-Uri-Path: /photos/rewrite2/index.html <br />
```

この結果から、以下のことが確認できました。 

- `path` と `query` は独立してそれぞれ上書きされる

# Request Trace API による検証
Request Trace API を使って、どのルールにマッチするかを簡単にテストできます。

- [Request Trace · Cloudflare API Documentation](https://developers.cloudflare.com/api/operations/account-request-tracer-request-trace)

```shell:
export EMAIL='YOUR_EMAIL'
export APIKEY='YOUR_APIKEY'
export ACCOUNT_ID='YOUR_ACCOUNT_ID'
```

```shell:
curl -s --request POST \
  --url https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/request-tracer/trace \
  -H 'Content-Type: application/json' \
  -H "X-Auth-Email: $EMAIL" \
  -H "X-Auth-Key: $APIKEY" \
  --data '{
  "method": "GET",
  "protocol": "HTTP/1.1",
  "url": "https://example.com/photos/index.html?w=500&h=500"
}' | jq -r '.result.trace[]| select ( .step_name == "http_request_transform")|.trace[0].trace'
```

リライトフェーズ `http_request_transform` では、
以下の4つのルールがマッチすることが検証できます。

```json:trace_array.json
[
  {
    "step_name": "aaa",
    "type": "rule",
    "matched": true,
    "action_parameter": {
      "uri": {
        "query": {
          "expression": "regex_replace(http.request.uri.query, \"w=\", \"width=\")"
        }
      }
    },
    "expression": "(starts_with(http.request.uri.path, \"/photos/\"))",
    "description": "w query to width query",
    "action": "rewrite"
  },
  {
    "step_name": "bbb",
    "type": "rule",
    "matched": true,
    "action_parameter": {
      "uri": {
        "query": {
          "expression": "regex_replace(http.request.uri.query, \"h=\", \"height=\")"
        }
      }
    },
    "expression": "(starts_with(http.request.uri.path, \"/photos/\"))",
    "description": "h query to height query",
    "action": "rewrite"
  },
  {
    "step_name": "ccc",
    "type": "rule",
    "matched": true,
    "action_parameter": {
      "uri": {
        "path": {
          "expression": "regex_replace(http.request.uri.path, \"/photos/\", \"/photos/rewrite1/\")"
        }
      }
    },
    "expression": "(starts_with(http.request.uri.path, \"/photos/\"))",
    "description": "photos path to rewrite1 path",
    "action": "rewrite"
  },
  {
    "step_name": "ddd",
    "type": "rule",
    "matched": true,
    "action_parameter": {
      "uri": {
        "path": {
          "expression": "regex_replace(http.request.uri.path, \"/photos/\", \"/photos/rewrite2/\")"
        }
      }
    },
    "expression": "(starts_with(http.request.uri.path, \"/photos/\"))",
    "description": "photos path to rewrite2 path",
    "action": "rewrite"
  }
]
```

また、`jq` による JSON マージを使えば、Transform Rules の適用動作に従う形で、以下のように上書きされた最終的な `action_parameter` を確認することができます。

- [How to merge 2 JSON files including objects and arrays using jq? - Stack Overflow](https://stackoverflow.com/questions/72560468/how-to-merge-2-json-files-including-objects-and-arrays-using-jq)
- [複数のjsonファイルをjqを使ってmergeする | DevelopersIO](https://dev.classmethod.jp/articles/merge_multiple_json/)
- [jq Manual (development version)](https://jqlang.github.io/jq/manual/)
> Objects are added by merging, that is, inserting all the key-value pairs from both objects into a single combined object. If both objects contain a value for the same key, the object on the right of the + wins. (For recursive merge use the * operator.)

```shell:
curl -s --request POST \
  --url https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/request-tracer/trace \
  -H 'Content-Type: application/json' \
  -H "X-Auth-Email: $EMAIL" \
  -H "X-Auth-Key: $APIKEY" \
  --data '{
  "method": "GET",
  "protocol": "HTTP/1.1",
  "url": "https://example.com/photos/index.html?w=500&h=500"
}' | jq -r '.result.trace[]| select ( .step_name == "http_request_transform")|.trace[0].trace| reduce .[].action_parameter as $i ({}; . * $i)'
```

```json:merged_action_parameter.json
{
  "uri": {
    "query": {
      "expression": "regex_replace(http.request.uri.query, \"h=\", \"height=\")"
    },
    "path": {
      "expression": "regex_replace(http.request.uri.path, \"/photos/\", \"/photos/rewrite2/\")"
    }
  }
}
```

# まとめ
リライト処理はどうしても複雑になってしまいがちなことや、複数ルールがあった場合に意図しない動きになっていないかどうか等の確認は、実装のタイミングで確実にチェックしておきたいところかと思います。

今回は、Transform Rules の Rewrite URL rules をピックアップして適用動作の検証をしましたが、同様のルール適用動作は Cache Rules や Origin Rules でも採用されているため、Request Trace API による検証が役立ちます。

- [Cache Rules : Order and priority](https://developers.cloudflare.com/cache/how-to/cache-rules/#order-and-priority)
> Cache Rules are stackable. This means that multiple matching rules will be combined and applied. So if multiple cache rules match the same URL, then the features set in those cache rules will all be applied. If several matching rules set a value for the same setting, the value in the last matching rule wins. For an example of a similar scenario where multiple rules match, refer to the [Origin Rules FAQ](https://developers.cloudflare.com/rules/origin-rules/faq/#what-happens-if-more-than-one-origin-rule-matches-the-current-request).