OAuthとC#でマルチバイト文字を扱う

人工衛星bothttp://twitter.com/XI_V)を作ったときに嵌ったところをメモる。

まずはC#でのTwitterAPI使用例をググる

OAuth認証まわりをやってくれるOAuthBase.cs、Twitter投稿用のOAuthTwitter.csというライブラリが転がっているので、これを使えばSignature生成やトークン取得など面倒な処理を自力で実装しなくて良い。

OAuthBase.cs
oAuthTwitter.cs - zscreen - Advanced Image/Text/File utility that allows for region/window/full-screen screenshots, text services and file hosting - Google Project Hosting

問題はこの両ライブラリがマルチバイト文字のことをいっさい考慮していないことで、そのまま日本語をポストすると怒られてしまう。

protected void Page_Load(object sender, EventArgs e)
{
     string url = "";
     string xml = "";
     oAuthTwitter oAuth = new oAuthTwitter();
 
     if (Request["oauth_token"] == null)
     {
         //Redirect the user to Twitter for authorization.
         //Using oauth_callback for local testing.
        Response.Redirect(oAuth.AuthorizationLinkGet() + "&oauth_callback=http://localhost");
     }
     else
     {
         //Get the access token and secret.
         oAuth.AccessTokenGet(Request["oauth_token"]);
         if (oAuth.TokenSecret.Length > 0)
         {
             //POST Test
             url = "http://twitter.com/statuses/update.xml";
             //これはOK
             xml = oAuth.oAuthWebRequest(oAuthTwitter.Method.POST, url, 
                      "status=" + Server.UrlEncode("Hello,OAuth"));
             //これは401が返る
             xml = oAuth.oAuthWebRequest(oAuthTwitter.Method.POST, url, 
                      "status=" + Server.UrlEncode("ほげ"));

             //apiResponse.InnerHtml = Server.HtmlEncode(xml);
         }
     }
}

流れを追う

oAuthWebRequestの内部で、受け取った文字列をHttpUtility.UrlDecodeでデコードしたのち、OAuthBaseのUrlEncodeで再エンコードし、これを元にシグネチャを生成してWebRequestを投げている。

//Decode the parameters and re-encode using the oAuth UrlEncode method.
NameValueCollection qs = HttpUtility.ParseQueryString(postData);
postData = "";
foreach (string key in qs.AllKeys)
{
    if (postData.Length > 0)
    {
          postData += "&";
    }
    //デコードして
    qs[key] = HttpUtility.UrlDecode(qs[key]);
    //OAuthBaseクラスのメソッドで再エンコード
    qs[key] = this.UrlEncode(qs[key]);
    postData += key + "=" + qs[key];
}

んで、このOAuthBase.UrlEncodeの中を見るとこんな感じ。

/// <summary>
/// This is a different Url Encode implementation since the default .NET one outputs the percent encoding in lower case.
/// While this is not a problem with the percent encoding spec, it is used in upper case throughout OAuth
/// </summary>
/// <param name="value">The value to Url encode</param>
/// <returns>Returns a Url encoded string</returns>
protected string UrlEncode(string value) {
   StringBuilder result = new StringBuilder();

   foreach (char symbol in value) {
       if (unreservedChars.IndexOf(symbol) != -1) {
          result.Append(symbol);
       } else {
          result.Append('%' + String.Format("{0:X2}", (int)symbol));
       }
   }

   return result.ToString();
}

.NETのHttpUtility.UrlEncodeだとエンコード結果が小文字で返ってくるので、大文字で返す関数を実装したらしい。が、この関数にマルチバイト文字を与えると、本来UTF-8のパーセントエンコード

status=%E3%81%BB%E3%81%92

になってほしいところがUTF-16みたく

status=%307B%3052

になってしまう。Twitter側にシグネチャが一致しないと怒られてしまうのはこれが原因。

対応

ということで、マルチバイトに対応した形にUrlEncodeを書きなおしてやれば良いが、既に書いている人がいたので参考にする。

[観] C# で OAuth

protected string UrlEncode(string value, Encoding encode)
{
    StringBuilder result = new StringBuilder();
    byte[] data = encode.GetBytes(value);
    int len = data.Length;

    for (int i = 0; i < len; i++)
    {
        int c = data[i];
        if (c < 0x80 && unreservedChars.IndexOf((char)c) != -1)
        {
             result.Append((char)c);
        }
        else
        {
             result.Append('%' + String.Format("{0:X2}", (int)data[i]));
        }
    }

    return result.ToString();
}

OAuthBase.csに上のメソッドを追加し、OAuthTwitter.csのoAuthWebRequestを一行だけ変更。

//Decode the parameters and re-encode using the oAuth UrlEncode method.
NameValueCollection qs = HttpUtility.ParseQueryString(postData);
postData = "";
foreach (string key in qs.AllKeys)
{
    if (postData.Length > 0)
    {
          postData += "&";
    }
    qs[key] = HttpUtility.UrlDecode(qs[key]);
    //第二引数を追加
    qs[key] = this.UrlEncode(qs[key],System.Text.Encoding.UTF8);
    postData += key + "=" + qs[key];
}

すると、こんな感じで日本語ポストができる。

string url = "";
string xml = "";

OAuthTwitter oAuth = new OAuthTwitter();

oAuth.ConsumerKey = this.consumerKey;
oAuth.ConsumerSecret = this.consumerSecret;
oAuth.Token = this.token;
oAuth.TokenSecret = this.tokenSecret;

//POST Test
string mes = "ほげ";
string encmes = string.Format("status={0:s}",System.Web.HttpUtility.UrlEncode(mes));
url = "http://twitter.com/statuses/update.xml";
xml = oAuth.oAuthWebRequest(OAuthTwitter.Method.POST, url, encmes);

めでたし。
と、ここまで書いてこんな日本語対応ライブラリがあることに気づいた。
http://www.ipentec.com/document/document.aspx?page=csharp-use-twitter-api-oauth-library
最初からこれを使っていれば嵌らなかったけど、なぜ素のOAuthBase.csで動かないのかが一応把握できたので良いか。