HTTP レスポンスを記録・再生できる HTTP プロキシを作った

タイトルの通り HTTP レスポンスを記録・再生できる HTTP プロキシを作りました。

# 何が欲しかったか
お仕事でクローラーの開発をしているのですが、クローラーを使ったシステム全体の E2E テストを書くために外部サービスをモックして HTTP レスポンスを返す何かが欲しいなーと常々思っていました。

求めていた機能は以下の 4 つです。
**1. HTTP レスポンスを記録し、記録されたデータを元にレスポンスができる**
**2. 記録されたデータは簡単に編集できて任意の HTTP レスポンスをシミュレートできる**
**3. 特定の言語やテストフレームワークに依存させないために HTTP プロキシとして使える**
**4. HTTPS にも対応している**

テスト用途で使える HTTP クライアントをモックしてレスポンスを返すライブラリがあったり、 API レスポンスをシミュレートしてモックを作れるものは色々とあったのですがちょうどぴったり使えるものがなかなか見つからなかったので作ることにしました。

ちなみに今回の目的には合わなかったんですがマイクロサービスのテストに使える [mountebank](http://www.mbtest.org/) という OSS があり、これも任意のレスポンスの再生ができるのでめちゃくちゃ便利そうでした。
他にも JavaScript (ブラウザ、node.js) には限定されますが [Polly.JS](https://netflix.github.io/pollyjs/#/)  という netflix が作っているライブラリがあり、HTTP サーバーのスタブを作って任意のレスポンスを返すようにしたり、レスポンスタイムの時間を調整できてこちらも超絶便利そうです。
フィットする用途があればどちらもいつか使ってみたいです。

# 仕組み
いろいろと調べてみると [mitmproxy](https://mitmproxy.org/) という今回の目的に合った素晴らしい HTTP プロキシのソフトウェアを見つけました。
mitmproxy の "mitm" は *Man In The Middle attack* (中間者攻撃) から来ており、 SSL 通信をこのプロキシで終端させることで HTTPS のリクエスト・レスポンスであっても中身を操作することができるというすごい HTTP プロキシです！ (怖)

mitmproxy 自身には HTTP レスポンスの記録・再生の機能はないのですが、リクエスト受信時やレスポンス送信時などの様々なポイントに任意の Python スクリプトを差し込めるという非常に便利な機能があり拡張できるようになっているので、その機構を利用して HTTP レスポンスを記録・再生をするためのプラグインを作りました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43699/fad82d77-1a09-f07e-4940-60ba0ee53e13.png)

ソースコードはこちらで公開しています。
https://github.com/Chanmoro/record-and-replay-proxy

簡単に使えるように Docker イメージを公開しています。
https://hub.docker.com/r/chanmoro/record-and-replay-proxy

# 使い方
Docker イメージを [Docker Hub で公開](https://hub.docker.com/r/chanmoro/record-and-replay-proxy) しているので簡単に使うことができます。

## 1. HTTP レスポンスの記録
以下のコマンドでレコーディングモードで HTTP プロキシを起動します。

```console
$ docker run -it --rm -p 8080:8080 -v ${PWD}/response_data:/app/response_data chanmoro/record-and-replay-proxy record
```

ここで記録されたレスポンスは `/app/response_data` に保存されるためローカルのディレクトリをマウントしてデータが残るようにします。

## 2. HTTP リクエストを送る (記録)
ここでは例として curl を使って https://github.com/ にアクセスした時のレスポンスを保存します。

```bash
$ curl -k -x localhost:8080 https://github.com/
```

この時 curl から SSL 通信に使われるのは mitmproxy が発行している自己証明書でデフォルトの状態でリクエストを送ると SSL の正当性チェックでエラーになるのでチェックをオフにします。
※ちょっと惜しい感じがしますがここはやむなし

### 記録された HTTP レスポンス
記録された HTTP レスポンスは Docker コンテナにマウントしたカレントディレクトリの `response_data` に保存されます。

```console
$ tree response_data
response_data
└── https%3A%2F%2Fgithub.com%2F # リクエスト URL がディレクトリ名になる
    └── GET                     # HTTP メソッド
        ├── metadata            # HTTP メソッド、リクエスト URL、HTTP レスポンスステータス
        ├── response_body       # レスポンスボディ
        └── response_header     # レスポンスヘッダ
```

`metadata` には記録したリクエストの HTTP メソッド、リクエスト URL、HTTP レスポンスステータスが保存されます。

``` json:metadata
{
    "url": "https://github.com/",
    "method": "GET",
    "status": 200
}
```

`response_body` にはレスポンスボディが記録されます。

```html:response_body
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
  <link rel="dns-prefetch" href="https://github.githubassets.com">
  <link rel="dns-prefetch" href="https://avatars0.githubusercontent.com">
  <link rel="dns-prefetch" href="https://avatars1.githubusercontent.com">
  <link rel="dns-prefetch" href="https://avatars2.githubusercontent.com">
  <link rel="dns-prefetch" href="https://avatars3.githubusercontent.com">
  <link rel="dns-prefetch" href="https://github-cloud.s3.amazonaws.com">
...
```

`response_header` にはレスポンスヘッダが記録されます。

```plaintext:response_header
Date: Thu, 12 Dec 2019 10:20:49 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Server: GitHub.com
Status: 200 OK
Vary: X-PJAX
...
```


## 3. 記録した HTTP レスポンスを再生する
記録した HTTP レスポンスを再生するにはリプレイモードで HTTP プロキシを起動します。
`/app/response_data` にマウントするディレクトリは、記録されたレスポンスのデータが入っているディレクトリを指定します。

```console
$ docker run -it --rm -p 8080:8080 -v ${PWD}/response_data:/app/response_data chanmoro/record-and-replay-proxy replay
```

## 4. HTTP リクエストを送る (再生)
リプレイモードで HTTP プロキシを起動した状態で記録時と同様に HTTP リクエストを送ると記録されたレスポンスが帰ってきます。
リプレイモード時にはプロキシから `https://github.com/` にはアクセスしないためオフラインの状態で使えます。

```bash
$ curl -k -x localhost:8080 https://github.com/
```

リプレイされていることが確認できるようにレスポンスデータを編集してみます。
データを以下の内容にそれぞれ編集します。

``` json:metadata
{
    "url": "https://github.com/",
    "method": "GET",
    "status": 500
}
```

```html:response_body
This is recorded response!
```

```plaintext:response_header
Date: Thu, 12 Dec 2019 10:20:49 GMT
Status: 500 Error
Hoge: This is Hoge header.
```

編集した通りにレスポンスが返されるか確認してみます。

```console
$ curl -v -k -x localhost:8080 https://github.com/
...
> GET / HTTP/1.1
> Host: github.com
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 500 Internal Server Error
< Date: Thu, 12 Dec 2019 10:20:49 GMT
< Status: 500 Error
< Hoge: This is Hoge header.
< content-length: 26
< 
* Connection #0 to host localhost left intact
This is recorded response!
```

HTTP ステータス、レスポンスヘッダ、レスポンスボディがそれぞれ編集した内容の通りになっていることがわかりますね。
最高！

# まとめ
この記事では作った HTTP レスポンスを記録・再生できる HTTP プロキシ [record-and-replay-proxy](https://github.com/Chanmoro/record-and-replay-proxy) についての機能と使い方をご紹介しました。

これで外部サービスや API にアクセスする機能のテストが簡単にできるようになります。

ぜひ使ってみてください！