(2015/02/09更新) フラグの持ち方に問題があったため完成版を修正

VolleyでTumblrにOAuthリクエスト

自作Tumblrクライアントアプリの通信周りは、oauth-signpostを使った自前実装で行っていました。
しかし、エラーハンドリングの容易さの魅力と、単純に使ってみたいと思ったという理由から実装をVolleyに書き換えるための調査を行いました。
やはり需要はあるようで、HurlStackを拡張してOAuthの署名を行う先人の知恵を幾つか見つけることができました。

しかし、これらの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

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

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

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

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回実行してしまっても問題ないだろうと判断し署名前に設定することにしました。

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使った実装してたから分かったけど、やってなかったら途中で挫折しただろうな…


今回作ったものはGistsにも上げているので、不具合問題あればプルリク大歓迎です。
OAuth signed POST request with Volley + oauth-signpost

自作のTumblrクライアントもGoogle Playで公開しているのでよろしくお願いします。
Android app on Google Play