RStudioのログイン画面をGoで突破する

※この投稿はRStudio Advent Calendar 2016 16日目の記事です。

銀杏BOYZ峯田和伸の名言でこんなことばがあります。

僕は思うんだ。本当の芸術というのは、音楽にしたって映画にしたって文章にしたって演芸にしたってなんだって、ドアが開かぬままあなたに会いに行ける魔法だって!

ドアが開かぬまま。

そうは言いつつもドアを開けたい。そんな気持ちになることもあるじゃないですか。

例えばこのドア。もとい、ログイン画面。

f:id:yutannihilation:20161214232239p:plain:w450

ここをRでこじ開ける方法はちょっと前にブログに書きました。

これをGoでやってみます。(Goは初心者同然なので、変なとこがあれば優しくツッコミを入れてもらえるとうれしいです><)

RStudioのログインの流れのおさらい

ここまでRStudio Advent Calenderの記事を追いかけてきたRStudioフェチの皆さんであればもう常識だと思いますが()、念のためRStudio Serverのログインの流れを確認しておきます。

  1. /auth-public-keyにGETリクエストを送って公開鍵を取得
  2. それを使ってユーザ名とパスワードを暗号化
  3. 暗号化した文字列を/auth-do-sign-inにPOSTリクエストで送ってcookieを取得

ということで、この処理をユーザに先回りしてやって、cookieだけつけてリダイレクトしてくれるプログラムをつくります。

公開鍵を取ってくる

まずは公開鍵をとってきましょう。エラー処理を省くとこんな感じです。

req, _ := http.NewRequest("GET", "http://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", "http://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", "http://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ヘッダはついていないのであてが外れました。

この挙動を変更する方法もあるんですが、推奨されたやり方なのかいまいち確信が持てません(詳しい方教えてください...)。

stackoverflow.com

今回は、Cookieを抜き出せばいいだけなので、以下の方法が使えます。cookieはリダイレクト前のものも含めてcookiejarというものに保存されます。デフォルトのhttp.DefaultClientではなく、こちらで用意したcookiejarを使うhttp.Clientでリクエストを投げれば、あとからほしいcookieを抜き出すことができます。

stackoverflow.com

こんな感じです。

jar, _ := cookiejar.New(nil)
client := http.Client{Jar: jar}

resp, _ := client.Do(req)
defer resp.Body.Close()

求めるcookieを抜き出す

ここからほしいcookieを抜き出します。localhostについているcookieはひとつだけのはず。

localhostURL, _ := url.Parse("http://localhost")
cookies := jar.Cookies(localhostURL)
if len(cookies) != 1 {
    return nil, fmt.Errorf("Unexpected Cookie: %#v", cookies)
}

loginSessionCookie := cookies[0]

cookieをつけてリダイレクトする

こんな感じのをhttp.HandleFunchandlerに指定すれば、cookieをつけてhttp://アクセスしてきた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です。

github.com

Dockerイメージもつくりました。

https://hub.docker.com/r/yutannihilation/tidyverse-open/

Dockerイメージを立ち上げて、

docker run -d -p 8787:8787 -p 80:80 yutannihilation/tidyverse-open

http://localhost/ にアクセスすると直接RStudio Serverが見えるはずです。

f:id:yutannihilation:20161215095214p:plain:w450

まとめ

ということで、RStudio Serverのログイン画面を突破するためのGoのプログラムを書いたという誰得な記事でした(すみません...)。

なんだこのゲテモノ記事は! けしからん!という義憤に駆られる方はぜひ、その怒りを記事としてぶつけてください。目には目を、記事には記事を。RStudio Advent Calender、まだ空いてますよ。

qiita.com

明日の担当はikuyaさんです。楽しみ!