※この投稿はRStudio Advent Calendar 2016 16日目の記事です。
僕は思うんだ。本当の芸術というのは、音楽にしたって映画にしたって文章にしたって演芸にしたってなんだって、ドアが開かぬままあなたに会いに行ける魔法だって!
ドアが開かぬまま。
そうは言いつつもドアを開けたい。そんな気持ちになることもあるじゃないですか。
例えばこのドア。もとい、ログイン画面。
ここをRでこじ開ける方法はちょっと前にブログに書きました。
これをGoでやってみます。(Goは初心者同然なので、変なとこがあれば優しくツッコミを入れてもらえるとうれしいです><)
RStudioのログインの流れのおさらい
ここまでRStudio Advent Calenderの記事を追いかけてきたRStudioフェチの皆さんであればもう常識だと思いますが()、念のためRStudio Serverのログインの流れを確認しておきます。
/auth-public-key
にGETリクエストを送って公開鍵を取得- それを使ってユーザ名とパスワードを暗号化
- 暗号化した文字列を
/auth-do-sign-in
にPOSTリクエストで送ってcookieを取得
ということで、この処理をユーザに先回りしてやって、cookieだけつけてリダイレクトしてくれるプログラムをつくります。
公開鍵を取ってくる
まずは公開鍵をとってきましょう。エラー処理を省くとこんな感じです。
req, _ := http.NewRequest("GET", "https://localhost:8787/auth-public-key", nil) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("Failed to get pubkey from %s", "https://localhost:8787/auth-public-key") } body, err := ioutil.ReadAll(resp.Body)
公開鍵をrsa.PublicKey
に変換
RStudio Server(というかGoogle Web Toolkit)の公開鍵は010001:DB1E3A8360F...
というような形式になっています。010001
の部分はpublic exponent、DB1E3A8360F...
の部分はmodulusです。rsa.Publickey
はちょうどこの情報を使います。この辺は前にちょこっと記事に書きました。
これもエラー処理を省くとこんな感じ。":"
で分割してそれぞれをパースします。
s := strings.Split(body, ":") # exponentはintなのでふつうにParseIntするだけ publicExponent, _ := strconv.ParseInt(s[0], 16, 0) # modulusはbig.Intなので少し手順が違う modulus := new(big.Int) modulus.SetString(s[1], 16) pubkey := &rsa.PublicKey{N: modulus, E: int(publicExponent)}
公開鍵で文字列を暗号化
RStudioはユーザ名とパスワードを改行でつなげたものをRSA PKCS#1 v1.5で暗号化します。そのままだとバイナリなので、BASE64エンコードします(これを忘れていて1時間くらいハマった…)。
vBin, _ := rsa.EncryptPKCS1v15(rand.Reader, pubkey, []byte(username+"\n"+password)) vStr := string(base64.StdEncoding.EncodeToString(vBin))
POSTリクエストを投げる
さっき生成したv
と、あと必要な項目を適当に埋めてurl.Values
に入れます。
form := url.Values{} form.Add("persist", "0") form.Add("appUri", "") form.Add("clientPath", "") form.Add("v", vStr)
次に、/auth-do-sign-in
に対してPOSTリクエストを投げるhttp.Request
を作ります。
req, err := http.NewRequest("POST", "https://localhost:8787/auth-do-sign-in", strings.NewReader(form.Encode())) # User-Agentはごまかす必要がある req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
で、これでリクエストを投げてレスポンスからSet-Cookie
ヘッダを抜き出せば終わりだと思ってたんですが、ログイン成功後にリダイレクトがあって、Goは自動でそのリダイレクト先にリクエストを投げます。リダイレクト先のレスポンスにはもうSet-Cookie
ヘッダはついていないのであてが外れました。
この挙動を変更する方法もあるんですが、推奨されたやり方なのかいまいち確信が持てません(詳しい方教えてください...)。
今回は、Cookieを抜き出せばいいだけなので、以下の方法が使えます。cookieはリダイレクト前のものも含めてcookiejar
というものに保存されます。デフォルトのhttp.DefaultClient
ではなく、こちらで用意したcookiejar
を使うhttp.Client
でリクエストを投げれば、あとからほしいcookieを抜き出すことができます。
こんな感じです。
jar, _ := cookiejar.New(nil) client := http.Client{Jar: jar} resp, _ := client.Do(req) defer resp.Body.Close()
求めるcookieを抜き出す
ここからほしいcookieを抜き出します。localhost
についているcookieはひとつだけのはず。
localhostURL, _ := url.Parse("https://localhost") cookies := jar.Cookies(localhostURL) if len(cookies) != 1 { return nil, fmt.Errorf("Unexpected Cookie: %#v", cookies) } loginSessionCookie := cookies[0]
cookieをつけてリダイレクトする
こんな感じのをhttp.HandleFunc
のhandler
に指定すれば、cookieをつけてhttps://アクセスしてきたURL:8787
、つまりRStudio Serverにリダイレクトしてくれます!
func(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, cookie) hostParts := strings.Split(r.Host, ":") publicURL := &url.URL{ Scheme: "http", Host: hostParts[0] + ":8787", Path: "/", } http.Redirect(w, r, publicURL.String(), 302) }
使い方
そんな感じで作ったのがこのrstudioexposer
です。
Dockerイメージもつくりました。
https://hub.docker.com/r/yutannihilation/tidyverse-open/
Dockerイメージを立ち上げて、
docker run -d -p 8787:8787 -p 80:80 yutannihilation/tidyverse-open
https://localhost/ にアクセスすると直接RStudio Serverが見えるはずです。
まとめ
ということで、RStudio Serverのログイン画面を突破するためのGoのプログラムを書いたという誰得な記事でした(すみません...)。
なんだこのゲテモノ記事は! けしからん!という義憤に駆られる方はぜひ、その怒りを記事としてぶつけてください。目には目を、記事には記事を。RStudio Advent Calender、まだ空いてますよ。
明日の担当はikuyaさんです。楽しみ!