Posted by & filed under programming, windows.


前回の続き。

Incorrect signatureと言われて投稿出来ていなかったが、1日の試行錯誤の末、解決した。
仕様をちゃんと読んでなかったのが原因。

前回のソースでは、URLエンコードを単純に、 System::Web::HttpUtility::UrlEncode メソッドを使って行っていたが、OAuthの仕様書には次のようにある。

この仕様書では以下のようにパーセントエンコーディング手法を定義する。

  1. テキスト値がまだ [RFC3629] で定義される UTF-8 オクテットにエンコードされていない場合は、そのようにエンコードする。人間に利用されないバイナリ値は、この行程をスキップする。
  2. 値を以下の [RFC3986] で定義されたパーセントエンコーディング手法に従ってエスケープする。
    • [RFC3986] のセクション2.3で定義された予約済みでない文字 (アルファベット, 数字, “-“, “.”, “_”, “~”) はエンコードしてはならない (MUST NOT)。
    • その他の文字は全てエンコードしなければならない (MUST)。
    • エンコード文字を示す2文字の16進数値は大文字でなければならない (MUST)。

via: The OAuth 1.0 Protocol draft-hammer-oauth-10

また、それに続けて次のようにも…

この手法は application/x-www-form-urlencoded で用いられるエンコード方式とは異なる。(例えばこの手法ではスペースは + ではなく %20 とエンコードされる) この手法は Web デベロップメントフレームワークが提供するパーセントエンコーディングとも異なる可能性がある (MAY)。(異なる文字をエンコードしたり、小文字の16進数文字を使うことなどが想定される)
via: The OAuth 1.0 Protocol draft-hammer-oauth-10

初めからきちんと読んでおけばこんな苦労することには…
Document はちゃんと読もう。

案の定、 HttpUtility::UrlEncode はスペースを+にエンコードし、さらに小文字の16進文字を使用していた。そのせいで Incorrect signature となっていた。
そこで、Tweenの作者さんの公開しているソースを参考に、自分でメソッドを作成。

String^ TwitterStatus::UrlEncode(String^ str)
{
	String^ UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
	StringBuilder^ sb = gcnew StringBuilder();
	array<Byte>^ bytes = Encoding::UTF8->GetBytes(str);

	for each(Byte b in bytes) {
		if (UnreservedChars->IndexOf(Convert::ToChar(b)) != -1) {
			sb->Append(Convert::ToChar(b));
		} else {
			sb->AppendFormat("%{0:X2}", b);
		}
	}
	return sb->ToString();
}

あと、 Signature base stringの組み立て方も間違ってたのでそこも修正。POSTでサーバーに送信する部分もURLエンコードをし忘れていたので修正。
すると、やっとまともに投稿出来るようになった。

--- TwitterStatus_old.cpp       2010-05-17 01:12:19.000000000 +0900
+++ TwitterStatus.cpp   2010-05-17 00:04:11.000000000 +0900
@@ -4,18 +4,19 @@
 TwitterStatus::TwitterStatus(String^ token, String^ secret)
 {
        this->authArgs = gcnew SortedList;
-       this->authArgs->Add("oauth_consumer_key", HttpUtility::UrlEncode(CONSUMER_KEY));
-       this->authArgs->Add("oauth_signature_method", HttpUtility::UrlEncode("HMAC-SHA1"));
-       this->authArgs->Add("oauth_token", HttpUtility::UrlEncode(token));
-       this->authArgs->Add("oauth_version", HttpUtility::UrlEncode("1.0"));
+       this->authArgs->Add("oauth_consumer_key", UrlEncode(CONSUMER_KEY));
+       this->authArgs->Add("oauth_signature_method", UrlEncode("HMAC-SHA1"));
+       this->authArgs->Add("oauth_token", UrlEncode(token));
+       this->authArgs->Add("oauth_version", UrlEncode("1.0"));

        // Create composite signing key
-       this->signing_key = HttpUtility::UrlEncode(CONSUMER_SECRET) + "&" + HttpUtility::UrlEncode(secret);
+       this->signing_key = UrlEncode(CONSUMER_SECRET) + "&" + UrlEncode(secret);
 }

 Void TwitterStatus::Post(String^ status)
 {
        UTF8Encoding^ encoding = gcnew UTF8Encoding;
+       status = "status=" + UrlEncode(status);

        // Get UNIX timestamp
        TimeSpan _TimeSpan = (DateTime::UtcNow - DateTime(1970, 1, 1, 0, 0, 0));
@@ -23,26 +24,27 @@
        // Create random string
        String^ guid = Convert::ToString(System::Guid::NewGuid());
        guid = guid->Replace("-", String::Empty);
-       this->authArgs->Add("oauth_nonce", HttpUtility::UrlEncode(guid));
-       // Generate signature
+       this->authArgs->Add("oauth_nonce", UrlEncode(guid));
+       // -- Generate signature --
        // Create signature base string
        StringBuilder^ signature_base_string = gcnew StringBuilder();
        signature_base_string->Append("POST&");
-       signature_base_string->Append(HttpUtility::UrlEncode(TWITTER_POST_URI));
+       signature_base_string->Append(UrlEncode(TWITTER_POST_URI));
        signature_base_string->Append("&");
+       StringBuilder^ query_str = gcnew StringBuilder();
        for each(DictionaryEntry^ arg in this->authArgs) {
-               signature_base_string->Append((String^)arg->Key);
-               signature_base_string->Append("%3D");
-               signature_base_string->Append((String^)arg->Value);
-               signature_base_string->Append("%26");
+               query_str->Append((String^)arg->Key);
+               query_str->Append("=");
+               query_str->Append((String^)arg->Value);
+               query_str->Append("&");
        }
-       signature_base_string->Append("status%3D");
-       signature_base_string->Append(HttpUtility::UrlEncode(status, Encoding::UTF8));
+       query_str->Append(status);
+       signature_base_string->Append(UrlEncode(query_str->ToString()));

        // Generage oauth_signature
        System::Security::Cryptography::HMACSHA1^ hmacsha1 = gcnew System::Security::Cryptography::HMACSHA1(encoding->GetBytes(signing_key));
        array<Byte>^ signature = hmacsha1->ComputeHash(encoding->GetBytes(signature_base_string->ToString()));
-       this->authArgs->Add("oauth_signature", HttpUtility::UrlEncode(Convert::ToBase64String(signature)));
+       this->authArgs->Add("oauth_signature", UrlEncode(Convert::ToBase64String(signature)));

        // -- Create authorization text --
        String^ authStr = "OAuth ";
@@ -53,9 +55,6 @@
        }
        authStr = authStr->Substring(0, authStr->Length -2);

-       // Convert status string
-       array<Byte>^ bytes = encoding->GetBytes("status=" + status);
-
        // Setup web request
        HttpWebRequest^ request = dynamic_cast<HttpWebRequest^>(WebRequest::Create(TWITTER_POST_URI));
        request->Proxy = request->GetSystemWebProxy();
@@ -65,15 +64,40 @@
        request->Headers->Add(HttpRequestHeader::Authorization, authStr);

        request->ContentType = "application/x-www-form-urlencoded";
-       request->ContentLength = bytes->Length;

        // post data
        IO::StreamWriter^ rqStream = gcnew IO::StreamWriter(request->GetRequestStream());
-       rqStream->Write("status="+status);
+       rqStream->Write(status);
        rqStream->Flush();
        rqStream->Close();

        this->authArgs->Remove("oauth_nonce");
        this->authArgs->Remove("oauth_timestamp");
        this->authArgs->Remove("oauth_signature");
+
+       HttpWebResponse^ res = (HttpWebResponse^)request->GetResponse();
+       IO::StreamReader^ reader = gcnew IO::StreamReader(res->GetResponseStream());
+#ifdef _DEBUG
+       System::Windows::Forms::MessageBox::Show(reader->ReadToEnd(), "DEBUG Information - Klab POS", System::Windows::Forms::MessageBoxButtons::OK, System::Windows::Forms::MessageBoxIcon::Information);
+#else
+       reader->ReadToEnd();
+#endif
+       reader->Close();
+       res->Close();
+}
+
+String^ TwitterStatus::UrlEncode(String^ str)
+{
+       String^ UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
+       StringBuilder^ sb = gcnew StringBuilder();
+       array<Byte>^ bytes = Encoding::UTF8->GetBytes(str);
+
+       for each(Byte b in bytes) {
+               if (UnreservedChars->IndexOf(Convert::ToChar(b)) != -1) {
+                       sb->Append(Convert::ToChar(b));
+               } else {
+                       sb->AppendFormat("%{0:X2}", b);
+               }
+       }
+       return sb->ToString();
 }
 No newline at end of file

OAuthは、signatureの生成が面倒くさいだけで、後はどのコマンドも同じような感じに使えるので、これで一通りはtwitter APIが利用出来るようになった…ハズ。
今回は投稿だけだけど、そのうちC#か何かを勉強して、自分で使う用のクライアントを自作してみようかと思う。