Volley + oauth-signpostでPOSTリクエスト


過去に自分の[ブログ](http://blog.chibatching.com/blog/2014/10/15/oauth-signed-post-with-volley-oauth-signpost/)に書いたものをQiitaにも転載。
***
完成版のフラグの扱いがまずそうだったので修正。
***
自作Tumblrクライアントアプリの通信周りは、oauth-signpostを使った自前実装で行っていました。

しかし、エラーハンドリングの容易さと、単純に使ってみたいと思ったという理由から実装をVolleyに書き換えるための調査を行いました。

やはり需要はあるようで、HurlStackを拡張してOAuthの署名を行う先人の知恵を幾つか見つけることができました。

- [Android - Volley+oauth-signpostでOAuthリクエスト - Qiita](http://qiita.com/PyYoshi@github/items/fe4b0a72e98852724859)  
- [volley + signpost](https://gist.github.com/krrrr38/8140942)


しかし、これらのHurlStack拡張はGETリクエストに対しては有効に動作しますが、この方法でPOSTリクエストを行うと401が帰ってきてしまいます。

```
E/Volley﹕ [46076] BasicNetwork.performRequest: Unexpected response code 401 for http://api.tumblr.com/v2/blog/hogehoge.tumblr.com/post/reblog
```


## 問題

POSTリクエストの場合、署名する前にconsumerにPOSTパラメータ等を渡してあげる必要がありますが、前述の例では開いたコネクションにすぐ署名しているためパラメータが足りていない状態です。

そこで、さらに前述のHurlStack拡張を修正していこうということになるのですが、これが意外に苦労しました。

まず、POSTリクエストを署名するにあたって、リクエストのパラメータが必要になるのですが、前述のHurlStack拡張が署名を行っている`createConnection`メソッドではVolleyのリクエストを参照できません。

では、他のメソッドはどうかと確認すると、そもそもOverride可能なのは`createConnection`のほかには`performRequest`だけです。

そして、`performRequest`にはVolleyリクエストが引数で渡されています。

なので、この2つのメソッドをOverrideしてPOSTリクエストを署名する方法を考えていきます。

## 試行錯誤

[HurlStack.java - platform/frameworks/volley - Git at Google](https://android.googlesource.com/platform/frameworks/volley/+/master/src/com/android/volley/toolbox/HurlStack.java)

HurlStack.javaを読んでみると、`performRequest`の中から`createConnection`を呼ぶことでコネクションを生成しているようです。

なので、Volleyリクエストが引数として渡されている`performRequest`内でOAuth用のパラメータをconsumerに設定する方法を試してみます。

なお、protectedな`Request#getParams()`にアクセスするため、自パッケージ内のRequest拡張にキャストしています。


```java
public class OAuthPostHurlStack extends HurlStack {

    private final OAuthConsumer mConsumer;

    public OAuthPostHurlStack(OAuthConsumer consumer) {
        mConsumer = consumer;
    }

    @Override
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
            throws IOException, AuthFailureError {

        if (request.getMethod() == Request.Method.POST) {
            if (request instanceof CustomRequest) {
                // POSTパラメータをHttpParametersに詰め替える
                HttpParameters oauthParams = new HttpParameters();
                Map<String, String> params = ((CustomRequest) request).getParams();
                for (String key : params.keySet()) {
                    oauthParams.put(key, OAuth.percentEncode(params.get(key)));
                }
                // POSTパラメータに加えてエンドポイントのURLも必要
                oauthParams.put("realm", request.getUrl());
                // consumerのパラメータとして設定
                mConsumer.setAdditionalParameters(oauthParams);
            }
        }

        return super.performRequest(request, additionalHeaders);
    }

    @Override
    protected HttpURLConnection createConnection(URL url) throws IOException {
        // 最初に紹介した先人のものと一緒なので省略
        return connection;
    }
}
```

これでうまくいくのでは？と思い実行してみるもまたも401エラー…なにかが足りない…？

```
E/Volley﹕ [46076] BasicNetwork.performRequest: Unexpected response code 401 for http://api.tumblr.com/v2/blog/hogehoge.tumblr.com/post/reblog
```

## 解決

改めて、自前の実装とHurlStack.javaを見比べて不明な設定が無いか確認してみると、自前の実装では署名の前に、HttpURLConnectionの`setRequestMethod("POST")`と`setDoOutput(true)`を呼んでいました。

元のHurlStack.javaでは、当然ですがこの処理は`createConnection`より後ろで行われています。

この際、HurlStackを拡張するのではなく、元から書き換えてしまおうかと一瞬頭をよぎりましたが、この2つの処理は本来の処理の前に1回実行してしまっても問題ないだろうと判断し署名前に設定することにしました。

```java
public class OAuthPostHurlStack extends HurlStack {

    private final OAuthConsumer mConsumer;
    private ArrayList<String> mOauthSignedPosts = new ArrayList<>();

    public OAuthPostHurlStack(OAuthConsumer consumer) {
        mConsumer = consumer;
    }

    @Override
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
            throws IOException, AuthFailureError {

        if (request.getMethod() == Request.Method.POST) {
            if (request instanceof CustomRequest) {
                // OAuthで署名すべきPOSTリクエストのURLを格納する
                mOauthSignedPosts.add(request.getUrl());
                // POSTパラメータをHttpParametersに詰め替える
                HttpParameters oauthParams = new HttpParameters();
                Map<String, String> params = ((CustomRequest) request).getParams();
                for (String key : params.keySet()) {
                    oauthParams.put(key, OAuth.percentEncode(params.get(key)));
                }
                // POSTパラメータに加えてエンドポイントのURLも必要
                oauthParams.put("realm", request.getUrl());
                // consumerのパラメータとして設定
                mConsumer.setAdditionalParameters(oauthParams);
            }
        }

        return super.performRequest(request, additionalHeaders);
    }

    @Override
    protected HttpURLConnection createConnection(URL url) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        try {
            if (mOauthSignedPosts.contains(url)) {
                // POSTリクエストのコネクションは署名前に設定が必要
                connection.setRequestMethod("POST");
                connection.setDoOutput(true);
                mOauthSignedPosts.remove(url)
            }
            mConsumer.sign(connection);
        } catch (OAuthMessageSignerException e) {
            e.printStackTrace();
        } catch (OAuthExpectationFailedException e) {
            e.printStackTrace();
        } catch (OAuthCommunicationException e) {
            e.printStackTrace();
        }
        return connection;
    }
}
```

これで無事、POSTリクエストも署名することができました。


## まとめ

oauth-signpostでHttpURLConnectionのPOSTリクエストを署名するときは、署名の前に下記の2点が必要

1. POSTパラメータ+エンドポイントのURLをパラメータとしてconsumerに設定する
2. HttpURLConnectionの`setRequestMethod("POST")`と`setDoOutput(true)`を設定する

これ、一回自分でoauth-signpost使った実装してたから分かったけど、やってなかったら途中で挫折しただろうな…

<br>
今回作ったものはGistsにも上げています。
[OAuth signed POST request with Volley + oauth-signpost](https://gist.github.com/chibatching/a59e30def6956da04b2f)


自作のTumblrクライアントもGoogle Playで公開しているのでよろしくお願いします。  
<a href="https://play.google.com/store/apps/details?id=com.chibatching.reprintr"><img alt="Android app on Google Play" src="http://developer.android.com/images/brand/ja_app_rgb_wo_45.png" /></a>