chromedpでChromeの特定のリクエストをinterceptする（Chrome DevTools Protocol）

Chromeを起動して特定のURLに対するリクエストだけinterceptしてSAMLレスポンスを取り出すCLIをgolangで作りたかったのですが、割と苦戦したのでメモ。

# 概要
golangからChromeを操作しようとすると [agouti](https://github.com/sclevine/agouti) や [chromedp](https://github.com/chromedp/chromedp) が出てくるのですが、これらはChromeの操作を全部自動化しようという話がほとんどです。
今回自分は特定のページに飛ばしたあとユーザにある程度操作させて、特定のリクエストのときだけ処理したかったのですがググっても簡単には出来なかったので書いておきます。

# 詳細
具体的には以下のツールでAzure ADでのログインをユーザにやらせて、AWSにSAMLレスポンスを投げるときにそれだけ取り出したかったです。
https://github.com/knqyf263/azaws

記事はこれ
https://qiita.com/knqyf263/items/acca73ca4316b41d901f

結論から言うとchromedpを使いました。chromedpは[Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)を使ってChromeを色々操作できるgolangのライブラリです。[examples](https://github.com/chromedp/examples)もあるのでスクリーンショットを撮りたい、とかは簡単にできます。
ですが、chromedpでは何故かnetwork eventに関するcallback等が設定できません（2018/01/08現在）。
https://github.com/chromedp/chromedp/issues/180

Chrome DevTools Protocolのドキュメントを見ると `getRequestPostData` とかいかにも欲しいメソッドが定義されていて、これこれ！と思ったのですがchromedpでは使えません。
https://chromedevtools.github.io/devtools-protocol/tot/Network

それで困っていたのですが、リポジトリを眺めていたら[cdproto](https://github.com/chromedp/cdproto)というリポジトリの方にtypesやeventsは全て定義されていました。これは自動生成されているので全てあるっぽいです。
じゃあchromedpの拡張をすればすぐ出来そうだな、と思ってchromedpのソースコードを読んでいたのですが、冷静に考えて同じこと思ってる人いるでしょという気分になったのでNetworhkのevents名などでググったらIssueが見つかりました。
https://github.com/chromedp/chromedp/issues/252

自分が見つけた時点では2週間ほど前にそのまま使えるexampleを貼っていてくれた人がいて、それを丸ごとパクりました。12月に作ろうと思ったら途中でやめてたかもしれないのでラッキーでした。

あとはそれを改修して作ったコードが以下です。抜粋して必要な箇所のみ書いてるのでそのままでは動かないです。

```golang

func devToolHandler(s string, is ...interface{}) {
	go func() {
		for _, elem := range is {
			var msg cdproto.Message
			// The CDP messages are sent as strings so we need to convert them back
			json.Unmarshal([]byte(fmt.Sprintf("%s", elem)), &msg)
			msgChann <- msg
		}
	}()
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// create chrome instance
	c, err := chromedp.New(ctx,
		chromedp.WithRunnerOptions(
			runner.Flag("user-data-dir", userDataDir),
			runner.Flag("disable-infobars", true)),
		chromedp.WithLog(devToolHandler))
	if err != nil {
		return err
	}

	err = c.Run(ctx, network.Enable())
	if err != nil {
		return err
	}

	loginURL := `https://login.microsoftonline.com/dummy/saml2?SAMLRequest=dummy`

	err = c.Run(ctx, chromedp.Navigate(loginURL))
	if err != nil {
		return err
	}

	err = c.Run(ctx, chromedp.ActionFunc(func(_ context.Context, h cdp.Executor) error {
		for {
			var msg cdproto.Message
			select {
			case <-ctx.Done():
				return ctx.Err()
			case msg = <-msgChann:
			}

			switch msg.Method.String() {
			case "Network.requestWillBeSent":
				var reqWillSend network.EventRequestWillBeSent
				json.Unmarshal(msg.Params, &reqWillSend)
				if reqWillSend.Request.URL != "https://signin.aws.amazon.com/saml" {
					continue
				}
				form, err := url.ParseQuery(reqWillSend.Request.PostData)
				if err != nil {
					return errors.Wrap(err, "Failed to parse query")
				}
				samlResponse, ok := form["SAMLResponse"]
				if !ok || len(samlResponse) == 0 {
					return errors.Wrap(err, "No such key: SAMLResponse")
				}
                                // Use samlResponse
				return nil
			}
		}
	}))
	if err != nil {
		return errors.Wrap(err, "Faled to handle events")
	}

	// shutdown chrome
	if err = c.Shutdown(ctx); err != nil {
		return errors.Wrap(err, "Faled to shutdown Chrome")
	}

	// wait for chrome to finish the shutdown
	if err = c.Wait(); err != nil {
		return err
	}
	return nil
}
```

上の例ではAzure ADのログインURLをChromeで開いて、ユーザが手動でログインしたあとにAWSにSAMLレスポンスを投げるタイミングで処理しています。
Azureからのレスポンスを取っても良かったのですが、AWSにSAMLレスポンスを投げるところの方がパースが楽だったのでそっちを使っています。
同じ感じでやれば他のNetwork系のeventも取れますしSAML連携しているようなサイトの自動化なども簡単にできると思います。

Chromeの操作するだけならnode.jsとかの方が簡単だったと思いますが、配りやすさを考えるとやはりgolangでやりたかったのでchromedpで頑張りました。

# まとめ
Chromeの全自動化じゃなくて半自動化みたいなのをしたい時に逆に面倒という話でした。
Issueにそのまま動くexample書いてくれる人は神（ちなみに上のexampleは動かない）。