Chromeを起動して特定のURLに対するリクエストだけinterceptしてSAMLレスポンスを取り出すCLIをgolangで作りたかったのですが、割と苦戦したのでメモ。
概要
golangからChromeを操作しようとすると agouti や chromedp が出てくるのですが、これらはChromeの操作を全部自動化しようという話がほとんどです。
今回自分は特定のページに飛ばしたあとユーザにある程度操作させて、特定のリクエストのときだけ処理したかったのですがググっても簡単には出来なかったので書いておきます。
詳細
具体的には以下のツールでAzure ADでのログインをユーザにやらせて、AWSにSAMLレスポンスを投げるときにそれだけ取り出したかったです。
https://github.com/knqyf263/azaws
記事はこれ
https://qiita.com/knqyf263/items/acca73ca4316b41d901f
結論から言うとchromedpを使いました。chromedpはChrome DevTools Protocolを使ってChromeを色々操作できるgolangのライブラリです。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というリポジトリの方にtypesやeventsは全て定義されていました。これは自動生成されているので全てあるっぽいです。
じゃあchromedpの拡張をすればすぐ出来そうだな、と思ってchromedpのソースコードを読んでいたのですが、冷静に考えて同じこと思ってる人いるでしょという気分になったのでNetworhkのevents名などでググったらIssueが見つかりました。
https://github.com/chromedp/chromedp/issues/252
自分が見つけた時点では2週間ほど前にそのまま使えるexampleを貼っていてくれた人がいて、それを丸ごとパクりました。12月に作ろうと思ったら途中でやめてたかもしれないのでラッキーでした。
あとはそれを改修して作ったコードが以下です。抜粋して必要な箇所のみ書いてるのでそのままでは動かないです。
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は動かない)。